ソースを参照

feat:事件管理相关系列页面

luoy 2 日 前
コミット
b372ae8e2b

+ 1 - 1
src/views/count/channel/index.vue

@@ -6,5 +6,5 @@
 const AllChannel = defineAsyncComponent(() => import('./allChannel.vue'));
 const OtherChannel = defineAsyncComponent(() => import('./otherChannel.vue'));
 
-const channel = ref('2')
+const channel = ref('1')
 </script>

+ 19 - 2
src/views/count/channel/otherChannel.vue

@@ -3,7 +3,7 @@
     <div class="!overflow-auto px-1">
       <Lcard>
         <div class="flex justify-start items-center">
-          <el-icon class="ml-1" style="color: #333333; margin-right: 25px">
+          <el-icon @click="() => selectChannel = '1'" class="ml-1" style="cursor: pointer; color: #333333; margin-right: 25px">
             <ArrowLeftBold />
           </el-icon>
           <el-select v-model="selectChannel" style="width: 100px;">
@@ -320,7 +320,24 @@ const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue')
 const Lprogress = defineAsyncComponent(() => import('/@/components/LYcom/Lprogress/index.vue'));
 const { t } = useI18n();
 
-const selectChannel = ref('1') // 渠道选择
+const props = defineProps({
+  channel: {
+    type: String,
+    default: '1',
+  },
+});
+const emit = defineEmits(['update:channel'])
+
+// 渠道选择
+const selectChannel = computed({
+  get() {
+    return props.channel;
+  },
+  set(val) {
+    emit('update:channel', val);
+    console.log(val);
+  },
+})
 const timeGroup = ref('1')
 const timeRange: any = ref(null);
 watch(timeGroup, (newVal) => {

+ 189 - 0
src/views/count/featureUsage/eventMannage/AddEventModal.vue

@@ -0,0 +1,189 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    title="添加事件"
+    width="500px"
+    :before-close="handleClose"
+  >
+    <el-form 
+      :model="form" 
+      :rules="rules" 
+      ref="formRef"
+      label-width="100px"
+    >
+      <el-form-item label="选择应用" prop="selectedApps" required>
+        <el-select 
+          v-model="form.selectedApps" 
+          multiple 
+          placeholder="请选择应用"
+          style="width: 100%"
+        >
+          <el-option
+            v-for="app in appOptions"
+            :key="app.value"
+            :label="app.label"
+            :value="app.value"
+          />
+        </el-select>
+      </el-form-item>
+      
+      <el-form-item label="事件ID" prop="eventId" required>
+        <el-input 
+          v-model="form.eventId" 
+          placeholder="请输入事件ID"
+        />
+      </el-form-item>
+      
+      <el-form-item label="显示名称" prop="displayName" required>
+        <el-input 
+          v-model="form.displayName" 
+          placeholder="请输入显示名称"
+        />
+      </el-form-item>
+      
+      <el-form-item label="事件类型" prop="eventType" required>
+        <el-select 
+          v-model="form.eventType" 
+          placeholder="请选择事件类型"
+          style="width: 100%"
+        >
+          <el-option
+            v-for="type in eventTypeOptions"
+            :key="type.value"
+            :label="type.label"
+            :value="type.value"
+          />
+        </el-select>
+      </el-form-item>
+    </el-form>
+    
+    <div class="tips">
+      <div class="tips-content">
+        说明:<br>
+        事件标识符不可更改,与代码中"String event_id"保持一致:参数(key)无需手动注册,每个事件下最多支持100个参数同时计算。为保证数据准确性,请使用(英文、数字、下划线(_)、中划线(-)、小数点(.)及加号(+))定义事件标识符或属性标识符。
+      </div>
+    </div>
+    
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="handleClose">取消</el-button>
+        <el-button type="primary" @click="handleAdd">添加</el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, reactive } from 'vue'
+import type { FormInstance, FormRules } from 'element-plus'
+
+interface Props {
+  modelValue: boolean
+}
+
+interface Emits {
+  (e: 'update:modelValue', value: boolean): void
+  (e: 'add', data: any): void
+}
+
+const props = defineProps<Props>()
+const emit = defineEmits<Emits>()
+
+// 表单引用
+const formRef = ref<FormInstance>()
+
+// 可见性绑定
+const visible = computed({
+  get: () => props.modelValue,
+  set: (value) => emit('update:modelValue', value)
+})
+
+// 表单数据
+const form = reactive({
+  selectedApps: [] as string[],
+  eventId: '',
+  displayName: '',
+  eventType: '' // 0: 多参数类型事件, 1: 计算事件
+})
+
+// 应用选项
+const appOptions = [
+  { label: '教育行业Demo', value: 'education' },
+  { label: '游戏行业Demo', value: 'game' },
+  { label: '通用行业Demo', value: 'general' }
+]
+
+// 事件类型选项
+const eventTypeOptions = [
+  { label: '多参数类型事件', value: 0 },
+  { label: '计算事件', value: 1 }
+]
+
+// 表单验证规则
+const rules = reactive<FormRules>({
+  selectedApps: [
+    { required: true, message: '请选择应用', trigger: 'change' }
+  ],
+  eventId: [
+    { required: true, message: '请输入事件ID', trigger: 'blur' },
+    { pattern: /^[a-zA-Z0-9_.\-+]+$/, message: '只能包含字母、数字、下划线(_)、中划线(-)、小数点(.)及加号(+)', trigger: 'blur' },
+    { max: 128, message: '长度不能超过128个字符', trigger: 'blur' }
+  ],
+  displayName: [
+    { required: true, message: '请输入显示名称', trigger: 'blur' }
+  ],
+  eventType: [
+    { required: true, message: '请选择事件类型', trigger: 'change' }
+  ]
+})
+
+// 关闭弹窗
+const handleClose = () => {
+  visible.value = false
+  // 重置表单
+  form.selectedApps = []
+  form.eventId = ''
+  form.displayName = ''
+  form.eventType = ''
+  // 清除表单验证
+  formRef.value?.resetFields()
+}
+
+// 处理添加事件
+const handleAdd = async () => {
+  if (!formRef.value) return
+  
+  await formRef.value.validate((valid) => {
+    if (valid) {
+      emit('add', {
+        apps: form.selectedApps,
+        eventId: form.eventId,
+        displayName: form.displayName,
+        eventType: form.eventType
+      })
+      handleClose()
+    }
+  })
+}
+</script>
+
+<style scoped lang="scss">
+.tips {
+  background-color: #f5f7fa;
+  border-radius: 4px;
+  padding: 12px;
+  margin-top: 20px;
+  
+  .tips-content {
+    font-size: 12px;
+    color: #606266;
+    line-height: 1.5;
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+</style>

+ 184 - 0
src/views/count/featureUsage/eventMannage/BatchImportModal.vue

@@ -0,0 +1,184 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    title="批量导入事件"
+    width="500px"
+    :before-close="handleClose"
+  >
+    <el-form :model="form" label-width="80px">
+      <el-form-item label="选择应用" required>
+        <el-select 
+          v-model="form.selectedApps" 
+          multiple 
+          placeholder="请选择应用"
+          style="width: 100%"
+        >
+          <el-option
+            v-for="app in appOptions"
+            :key="app.value"
+            :label="app.label"
+            :value="app.value"
+          />
+        </el-select>
+      </el-form-item>
+      
+      <el-form-item label="选择文件">
+        <el-button @click="selectFile">
+          <el-icon><Upload /></el-icon>
+          选择Excel文件
+        </el-button>
+        <br>
+        <div v-if="form.fileName" class="file-name">{{ form.fileName }}</div>
+      </el-form-item>
+      
+      <el-form-item label=" ">
+        <el-button @click="downloadTemplate">
+          <el-icon><Download /></el-icon>
+          Excel模板下载
+        </el-button>
+      </el-form-item>
+    </el-form>
+    
+    <div class="tips">
+      <div class="tips-title">说明:</div>
+      <div class="tips-content">
+        <p>① 命名规范:事件标识符(event id)、事件属性标识符(key)命名支持字母(建议小写,避免使用中文)、数字、下划线 (_)、中划线 (-)、小数点 (.)及加号(+) 。event id和key长度不能超过128个字节。</p>
+        <p>② 事件类型,1表示计算事件,0表示多参数类型事件</p>
+        <p>③ 属性类型:number数值型(可计算),string字符串</p>
+      </div>
+    </div>
+    
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="handleClose">取消</el-button>
+        <el-button type="primary" @click="handleImport" :disabled="!canImport">导入</el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, reactive } from 'vue'
+import { Upload, Download } from '@element-plus/icons-vue'
+
+interface Props {
+  modelValue: boolean
+}
+
+interface Emits {
+  (e: 'update:modelValue', value: boolean): void
+  (e: 'import', data: any): void
+}
+
+const props = defineProps<Props>()
+const emit = defineEmits<Emits>()
+
+// 可见性绑定
+const visible = computed({
+  get: () => props.modelValue,
+  set: (value) => emit('update:modelValue', value)
+})
+
+// 表单数据
+const form = reactive({
+  selectedApps: [] as string[],
+  fileName: '',
+  file: null as File | null
+})
+
+// 应用选项
+const appOptions = [
+  { label: '教育行业Demo', value: 'education' },
+  { label: '游戏行业Demo', value: 'game' },
+  { label: '通用行业Demo', value: 'general' }
+]
+
+// 是否可以导入(已选择应用和文件)
+const canImport = computed(() => {
+  return form.selectedApps.length > 0 && form.file !== null
+})
+
+// 关闭弹窗
+const handleClose = () => {
+  visible.value = false
+  // 重置表单
+  form.selectedApps = []
+  form.fileName = ''
+  form.file = null
+}
+
+// 选择文件
+const selectFile = () => {
+  const input = document.createElement('input')
+  input.type = 'file'
+  input.accept = '.xlsx,.xls'
+  input.onchange = (e) => {
+    const file = (e.target as HTMLInputElement).files?.[0]
+    if (file) {
+      form.file = file
+      form.fileName = file.name
+    }
+  }
+  input.click()
+}
+
+// 下载模板
+const downloadTemplate = () => {
+  // 这里可以实现实际的模板下载逻辑
+  console.log('下载模板')
+  // 示例:创建一个虚拟的下载链接
+  const link = document.createElement('a')
+  link.href = 'data:application/vnd.ms-excel;base64,UEsDBAoAAAAAALJdHFYAAAAA' // 示例base64
+  link.download = '事件导入模板.xlsx'
+  link.click()
+}
+
+// 处理导入
+const handleImport = () => {
+  if (!canImport.value) return
+  
+  emit('import', {
+    apps: form.selectedApps,
+    file: form.file
+  })
+  
+  handleClose()
+}
+</script>
+
+<style scoped lang="scss">
+.file-name {
+  margin-top: 8px;
+  font-size: 14px;
+  color: #606266;
+}
+
+.tips {
+  background-color: #f5f7fa;
+  border-radius: 4px;
+  padding: 12px;
+  margin-top: 20px;
+  
+  .tips-title {
+    font-weight: bold;
+    margin-bottom: 8px;
+    color: #303133;
+  }
+  
+  .tips-content {
+    font-size: 12px;
+    color: #606266;
+    line-height: 1.5;
+    
+    p {
+      margin: 0 0 4px 0;
+    }
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+</style>

+ 511 - 0
src/views/count/featureUsage/eventMannage/EventEdit.vue

@@ -0,0 +1,511 @@
+<template>
+  <div class="layout-padding">
+    <div class="!overflow-auto px-1">
+      <Lcard style=" margin-bottom: 0px;">
+        <div class="flex justify-start items-center">
+          <el-icon @click="back" class="ml-1" style="cursor: pointer; color: #333333; margin-right: 10px">
+            <ArrowLeftBold />
+          </el-icon>
+          <Title title="编辑事件详情">
+          </Title>
+        </div>
+        <div style="margin: 20px 60px;">
+          <!-- 添加事件详情展示区域 -->
+          <div class="event-detail-card">
+            <!-- <div class="event-detail-header">
+              <h3>事件基本信息</h3>
+            </div> -->
+            <div class="event-detail-content">
+              <div class="detail-row">
+                <div class="detail-item">
+                  <span class="detail-label">事件名称:</span>
+                  <span class="detail-value">用户注册</span>
+                </div>
+                <div class="detail-item">
+                  <span class="detail-label">事件标识符:</span>
+                  <span class="detail-value">user_register</span>
+                </div>
+              </div>
+              <div class="detail-row">
+                <div class="detail-item">
+                  <span class="detail-label">计算状态:</span>
+                  <span class="detail-value">
+                    <el-tag type="success">计算中</el-tag>
+                  </span>
+                </div>
+                <div class="detail-item">
+                  <span class="detail-label">近三十天上报数:</span>
+                  <span class="detail-value">12,560</span>
+                </div>
+              </div>
+              <div class="detail-row">
+                <div class="detail-item">
+                  <span class="detail-label">上报平台:</span>
+                  <span class="detail-value">Android、IOS</span>
+                </div>
+                <div class="detail-item">
+                  <span class="detail-label">应用:</span>
+                  <span class="detail-value">教育行业Demo</span>
+                </div>
+              </div>
+              <div class="detail-row">
+                <div class="detail-item">
+                  <span class="detail-label">埋点触发时机:</span>
+                  <span class="detail-value">用户完成注册流程时触发</span>
+                </div>
+                <div class="detail-item">
+                  <span class="detail-label">操作人:</span>
+                  <span class="detail-value">nina</span>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div style="margin: 20px 60px;">
+          <el-radio-group v-model="eventTypeRadio">
+            <el-radio-button label="1">自定义属性</el-radio-button>
+            <el-radio-button label="2">预制全局属性</el-radio-button>
+          </el-radio-group>
+          <div class="line" style="margin: 0 0 20px 0;"></div>
+          <div class="flex search-box" style="margin-bottom: 20px;">
+            <el-radio-group v-model="eventTypeRadio">
+              <el-radio-button label="1">计算中事件</el-radio-button>
+              <el-radio-button label="2">暂停计算事件</el-radio-button>
+              <el-radio-button label="3">未注册事件</el-radio-button>
+            </el-radio-group>
+            <el-input placeholder="请输入事件名/事件标识符" style="width: 230px;"></el-input>
+            <el-button @click="handleAddProperty" style="margin-left: auto;" type="primary">新增属性</el-button>
+            <el-button>批量停止计算</el-button>
+          </div>
+          <el-table :height="700" :data="pagedTableRows" border>
+            <el-table-column prop="propertyName" label="属性名称" min-width="120" />
+            <el-table-column prop="propertyIdentifier" label="属性标识符" min-width="140" />
+            <el-table-column label="属性类型" min-width="100">
+              <template #default="scope">
+                <el-tag :type="getPropertyType(scope.row.propertyType)">{{ getPropertyTypeName(scope.row.propertyType)
+                }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="计算状态" min-width="100">
+              <template #default="scope">
+                <el-tag :type="getStatusType(scope.row.status)">{{ scope.row.status }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="remark" label="属性备注" min-width="120" show-overflow-tooltip />
+            <el-table-column label="应用" min-width="120">
+              <template #default="scope">
+                {{ getAppText(scope.row.apps) }}
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" min-width="160" fixed="right">
+              <template #default="scope">
+                <el-button @click="handleEditProperty(scope.row)" type="primary" link>编辑</el-button>
+                <!-- <el-button type="primary" link>详情</el-button> -->
+                <el-button v-if="scope.row.status === '计算中'" type="primary" link>暂停</el-button>
+                <el-button v-else-if="scope.row.status === '暂停计算'" type="primary" link>启用</el-button>
+                <el-button type="danger" link>删除</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+          <div class="flex justify-end mt-3">
+            <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" background
+              layout="total, prev, pager, next, sizes" :total="tableRows.length" :page-sizes="[5, 10, 20]" />
+          </div>
+        </div>
+      </Lcard>
+      <EventPropEditModal v-model="showPropertyModal" :is-edit="isEditProperty" :data="currentPropertyData"
+        @submit="handleSubmitProperty" />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" name="countMainTrend" setup>
+// import { ref, onMounted, watch, computed, defineAsyncComponent } from 'vue';
+import { useI18n } from 'vue-i18n';
+import * as echarts from 'echarts';
+// 引入组件
+const Lcard = defineAsyncComponent(() => import('/@/components/LYcom/Lcard/index.vue'));
+const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
+const EventPropEditModal = defineAsyncComponent(() => import('./EventPropEditModal.vue'))
+const { t } = useI18n();
+const eventTypeRadio = ref('1')
+
+onMounted(() => {
+  setTimeout(() => {
+  }, 500);
+});
+
+// 编辑属性相关
+const showPropertyModal = ref(false)
+const isEditProperty = ref(false)
+const currentPropertyData = ref<any>(null)
+
+// 在 script 部分添加
+const handleAddProperty = () => {
+  isEditProperty.value = false
+  currentPropertyData.value = null
+  showPropertyModal.value = true
+}
+
+const handleEditProperty = (data: any) => {
+  isEditProperty.value = true
+  currentPropertyData.value = data
+  showPropertyModal.value = true
+}
+
+const handleSubmitProperty = (data: any) => {
+  console.log('提交属性数据:', data)
+  // 这里可以实现实际的新增或编辑逻辑
+  // 例如发送请求到后端
+}
+
+const emit = defineEmits(['update:eventId'])
+
+const back = () => {
+  emit('update:eventId', null)
+}
+
+const searchData = reactive({
+  eventType: '1',
+  channel: '',
+  eventName: '',
+  eventHas: '',
+  appId: '',
+  evnetUser: '',
+})
+
+// 表格相关(静态数据)
+const currentPage = ref(1);
+const pageSize = ref(10);
+
+const pagedTableRows = computed(() => {
+  const startIndex = (currentPage.value - 1) * pageSize.value;
+  return tableRows.value.slice(startIndex, startIndex + pageSize.value);
+});
+
+
+// 修改表格数据结构
+interface EventTableRow {
+  id: string;
+  propertyName: string; // 属性名称
+  propertyIdentifier: string; // 属性标识符
+  propertyType: string; // 属性类型
+  status: string; // 计算状态
+  remark: string; // 属性备注
+  apps: string[]; // 应用
+}
+
+// 更新表格静态数据
+const tableRows = ref<EventTableRow[]>([
+  {
+    id: '1',
+    propertyName: '用户ID',
+    propertyIdentifier: 'user_id',
+    propertyType: 'string',
+    status: '计算中',
+    remark: '用户的唯一标识符',
+    apps: ['教育行业Demo', '游戏行业Demo']
+  },
+  {
+    id: '2',
+    propertyName: '访问时长',
+    propertyIdentifier: 'visit_duration',
+    propertyType: 'number',
+    status: '计算中',
+    remark: '用户单次访问的时长',
+    apps: ['教育行业Demo']
+  },
+  {
+    id: '3',
+    propertyName: '设备型号',
+    propertyIdentifier: 'device_model',
+    propertyType: 'string',
+    status: '暂停计算',
+    remark: '用户使用的设备型号',
+    apps: ['游戏行业Demo', '通用行业Demo']
+  },
+  {
+    id: '4',
+    propertyName: '页面路径',
+    propertyIdentifier: 'page_path',
+    propertyType: 'string',
+    status: '计算中',
+    remark: '用户访问的页面路径',
+    apps: ['教育行业Demo', '游戏行业Demo', '通用行业Demo']
+  },
+  {
+    id: '5',
+    propertyName: '点击次数',
+    propertyIdentifier: 'click_count',
+    propertyType: 'number',
+    status: '计算中',
+    remark: '页面元素被点击的次数',
+    apps: ['游戏行业Demo']
+  },
+  {
+    id: '6',
+    propertyName: '会话ID',
+    propertyIdentifier: 'session_id',
+    propertyType: 'string',
+    status: '未注册',
+    remark: '用户会话的唯一标识',
+    apps: ['通用行业Demo']
+  },
+  {
+    id: '7',
+    propertyName: '地理位置',
+    propertyIdentifier: 'geo_location',
+    propertyType: 'string',
+    status: '计算中',
+    remark: '用户的地理位置信息',
+    apps: ['教育行业Demo']
+  },
+  {
+    id: '8',
+    propertyName: '购买金额',
+    propertyIdentifier: 'purchase_amount',
+    propertyType: 'number',
+    status: '计算中',
+    remark: '用户购买商品的金额',
+    apps: ['游戏行业Demo', '通用行业Demo']
+  },
+  {
+    id: '9',
+    propertyName: '分享平台',
+    propertyIdentifier: 'share_platform',
+    propertyType: 'string',
+    status: '暂停计算',
+    remark: '用户分享到的平台',
+    apps: ['教育行业Demo']
+  },
+  {
+    id: '10',
+    propertyName: '搜索关键词',
+    propertyIdentifier: 'search_keyword',
+    propertyType: 'string',
+    status: '计算中',
+    remark: '用户搜索时使用的关键词',
+    apps: ['游戏行业Demo', '通用行业Demo']
+  },
+  {
+    id: '11',
+    propertyName: '错误代码',
+    propertyIdentifier: 'error_code',
+    propertyType: 'number',
+    status: '未注册',
+    remark: '系统错误代码',
+    apps: ['通用行业Demo']
+  },
+  {
+    id: '12',
+    propertyName: '网络类型',
+    propertyIdentifier: 'network_type',
+    propertyType: 'string',
+    status: '计算中',
+    remark: '用户当前的网络连接类型',
+    apps: ['教育行业Demo', '游戏行业Demo']
+  },
+  {
+    id: '13',
+    propertyName: '用户年龄',
+    propertyIdentifier: 'user_age',
+    propertyType: 'number',
+    status: '计算中',
+    remark: '用户的年龄信息',
+    apps: ['教育行业Demo']
+  },
+  {
+    id: '14',
+    propertyName: '操作系统',
+    propertyIdentifier: 'os_version',
+    propertyType: 'string',
+    status: '暂停计算',
+    remark: '设备的操作系统版本',
+    apps: ['游戏行业Demo', '通用行业Demo']
+  },
+  {
+    id: '15',
+    propertyName: '页面停留时间',
+    propertyIdentifier: 'page_stay_time',
+    propertyType: 'number',
+    status: '计算中',
+    remark: '用户在页面停留的时间',
+    apps: ['教育行业Demo', '游戏行业Demo', '通用行业Demo']
+  }
+]);
+
+// 获取属性类型标签样式
+const getPropertyType = (type: string) => {
+  switch (type) {
+    case 'string':
+      return 'primary';
+    case 'number':
+      return 'success';
+    default:
+      return 'info';
+  }
+};
+
+// 获取属性类型名称
+const getPropertyTypeName = (type: string) => {
+  switch (type) {
+    case 'string':
+      return '字符串';
+    case 'number':
+      return '数值型';
+    default:
+      return type;
+  }
+};
+
+// 获取应用显示文本(复用原来的方法)
+const getAppText = (apps: string[]) => {
+  if (apps.length === 0) return '-';
+  if (apps.length > 2) return `${apps[0]}等${apps.length}个`;
+  return apps.join('、');
+};
+
+// 状态标签样式(保持不变)
+const getStatusType = (status: string) => {
+  switch (status) {
+    case '计算中':
+      return 'success';
+    case '暂停计算':
+      return 'warning';
+    case '未注册':
+      return 'info';
+    default:
+      return 'info';
+  }
+};
+</script>
+<style lang="scss" scoped>
+.search-box>div {
+  margin-right: 20px;
+}
+
+/* 添加事件详情样式 */
+.event-detail-card {
+  border: 1px solid #e4e7ed;
+  border-radius: 4px;
+  margin-bottom: 20px;
+
+  .event-detail-header {
+    padding: 12px 20px;
+    border-bottom: 1px solid #e4e7ed;
+    background-color: #f5f7fa;
+
+    h3 {
+      margin: 0;
+      font-size: 16px;
+      font-weight: 600;
+      color: #303133;
+    }
+  }
+
+  .event-detail-content {
+    padding: 20px;
+
+    .detail-row {
+      display: flex;
+      margin-bottom: 16px;
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
+
+    .detail-item {
+      flex: 1;
+      display: flex;
+
+      .detail-label {
+        width: 120px;
+        font-size: 14px;
+        color: #606266;
+        text-align: right;
+        margin-right: 12px;
+        flex-shrink: 0;
+      }
+
+      .detail-value {
+        flex: 1;
+        font-size: 14px;
+        color: #303133;
+
+        :deep(.el-tag) {
+          margin-right: 0;
+        }
+      }
+    }
+  }
+}
+
+/* 响应式处理 */
+@media (max-width: 768px) {
+  .event-detail-card {
+    .event-detail-content {
+      .detail-row {
+        flex-direction: column;
+        margin-bottom: 12px;
+      }
+
+      .detail-item {
+        margin-bottom: 8px;
+
+        &:last-child {
+          margin-bottom: 0;
+        }
+
+        .detail-label {
+          width: auto;
+          text-align: left;
+          margin-right: 8px;
+        }
+      }
+    }
+  }
+}
+
+.msg {
+  height: 30px;
+  background-color: #f4f5fa;
+  line-height: 30px;
+  font-size: 12px;
+}
+
+.link {
+  color: #167af0;
+  cursor: pointer;
+}
+
+.box1 {
+  display: flex;
+  margin-top: 30px;
+
+  .card-box1,
+  .card-box2 {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: start;
+  }
+
+  .card-box1 {
+    .info-card {
+      width: 48%;
+    }
+  }
+
+  .card-box2 {
+    .info-card {
+      width: 33%;
+    }
+  }
+}
+
+.line {
+  margin: 60px -30px 30px;
+  height: 1px;
+  background-color: #E6E6E6;
+}
+</style>

+ 184 - 0
src/views/count/featureUsage/eventMannage/EventPropEditModal.vue

@@ -0,0 +1,184 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    :title="isEdit ? '编辑属性' : '新增属性'"
+    width="500px"
+    :before-close="handleClose"
+  >
+    <el-form 
+      :model="form" 
+      :rules="rules" 
+      ref="formRef"
+      label-width="100px"
+    >
+      <el-form-item label="属性名称" prop="propertyName" required>
+        <el-input 
+          v-model="form.propertyName" 
+          :disabled="isEdit"
+          placeholder="请输入属性名称"
+        />
+      </el-form-item>
+      
+      <el-form-item label="属性标识符" prop="propertyIdentifier" required>
+        <el-input 
+          v-model="form.propertyIdentifier" 
+          :disabled="isEdit"
+          placeholder="请输入属性标识符"
+        />
+        <div class="form-item-tip">支持字母、数字、下划线(_)、中划线(-)、小数点(.)及加号(+)</div>
+      </el-form-item>
+      
+      <el-form-item label="属性类型" prop="propertyType" required>
+        <el-select 
+          v-model="form.propertyType" 
+          :disabled="isEdit"
+          placeholder="请选择属性类型"
+          style="width: 100%"
+        >
+          <el-option label="字符串" value="string" />
+          <el-option label="数值型" value="number" />
+        </el-select>
+      </el-form-item>
+      
+      <el-form-item label="属性备注" prop="remark">
+        <el-input 
+          v-model="form.remark" 
+          type="textarea"
+          :rows="3"
+          placeholder="请输入属性备注"
+        />
+      </el-form-item>
+      
+      <el-form-item label="应用" prop="apps" required>
+        <el-select 
+          v-model="form.apps" 
+          multiple
+          placeholder="请选择应用"
+          style="width: 100%"
+        >
+          <el-option label="教育行业Demo" value="education" />
+          <el-option label="游戏行业Demo" value="game" />
+          <el-option label="通用行业Demo" value="general" />
+        </el-select>
+      </el-form-item>
+    </el-form>
+    
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="handleClose">取消</el-button>
+        <el-button type="primary" @click="handleSubmit">确定</el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, reactive, watch } from 'vue'
+import type { FormInstance, FormRules } from 'element-plus'
+
+interface Props {
+  modelValue: boolean
+  isEdit: boolean
+  data?: any
+}
+
+interface Emits {
+  (e: 'update:modelValue', value: boolean): void
+  (e: 'submit', data: any): void
+}
+
+const props = defineProps<Props>()
+const emit = defineEmits<Emits>()
+
+// 表单引用
+const formRef = ref<FormInstance>()
+
+// 可见性绑定
+const visible = computed({
+  get: () => props.modelValue,
+  set: (value) => emit('update:modelValue', value)
+})
+
+// 表单数据
+const form = reactive({
+  propertyName: '',
+  propertyIdentifier: '',
+  propertyType: '',
+  remark: '',
+  apps: [] as string[]
+})
+
+// 表单验证规则
+const rules = reactive<FormRules>({
+  propertyName: [
+    { required: true, message: '请输入属性名称', trigger: 'blur' }
+  ],
+  propertyIdentifier: [
+    { required: true, message: '请输入属性标识符', trigger: 'blur' },
+    { pattern: /^[a-zA-Z0-9_.\-+]+$/, message: '只能包含字母、数字、下划线(_)、中划线(-)、小数点(.)及加号(+)', trigger: 'blur' },
+    { max: 128, message: '长度不能超过128个字符', trigger: 'blur' }
+  ],
+  propertyType: [
+    { required: true, message: '请选择属性类型', trigger: 'change' }
+  ],
+  apps: [
+    { required: true, message: '请选择应用', trigger: 'change' }
+  ]
+})
+
+// 监听数据变化,用于编辑时填充表单
+watch(() => props.data, (newVal) => {
+  if (newVal && props.isEdit) {
+    form.propertyName = newVal.propertyName || ''
+    form.propertyIdentifier = newVal.propertyIdentifier || ''
+    form.propertyType = newVal.propertyType || ''
+    form.remark = newVal.remark || ''
+    form.apps = newVal.apps || []
+  }
+}, { immediate: true })
+
+// 重置表单
+const resetForm = () => {
+  form.propertyName = ''
+  form.propertyIdentifier = ''
+  form.propertyType = ''
+  form.remark = ''
+  form.apps = []
+  formRef.value?.resetFields()
+}
+
+// 关闭弹窗
+const handleClose = () => {
+  visible.value = false
+  resetForm()
+}
+
+// 处理提交
+const handleSubmit = async () => {
+  if (!formRef.value) return
+  
+  await formRef.value.validate((valid) => {
+    if (valid) {
+      emit('submit', {
+        ...form,
+        isEdit: props.isEdit
+      })
+      handleClose()
+    }
+  })
+}
+</script>
+
+<style scoped lang="scss">
+.form-item-tip {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 4px;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+</style>

+ 399 - 0
src/views/count/featureUsage/eventMannage/EventTable.vue

@@ -0,0 +1,399 @@
+<template>
+  <div class="layout-padding">
+    <div class="!overflow-auto px-1">
+      <Lcard style="height: calc(100vh - 96px); margin-bottom: 0px;">
+        <div class="flex justify-between items-center">
+          <Title left-line title="事件管理">
+          </Title>
+          <div>
+            <el-button @click="() => showBatchImportModal = true" icon="Download">批量导入事件</el-button>
+            <el-button @click="() => showAddEventModal = true" icon="Plus" type="primary">添加事件</el-button>
+          </div>
+        </div>
+        <div style="margin: 20px 60px;">
+          <el-radio-group v-model="eventTypeRadio">
+            <el-radio-button label="1">计算中事件</el-radio-button>
+            <el-radio-button label="2">暂停计算事件</el-radio-button>
+            <el-radio-button label="3">未注册事件</el-radio-button>
+          </el-radio-group>
+          <div class="line" style="margin: 0 0 20px 0;"></div>
+          <div class="flex search-box" style="margin-bottom: 20px;">
+            <el-input placeholder="请输入事件名/事件标识符" style="width: 230px;"></el-input>
+            <el-select placeholder="选择平台" v-model="selectLineChannel" clearable style="width: 140px;">
+              <el-option label="Android" value="1"></el-option>
+              <el-option label="IOS" value="2"></el-option>
+              <el-option label="IPad" value="3"></el-option>
+              <el-option label="Harmony" value="4"></el-option>
+            </el-select>
+            <el-select placeholder="近三十天事件上报数" v-model="selectLineChannel" clearable style="width: 140px;">
+              <el-option label="有数据" value="1"></el-option>
+              <el-option label="无数据" value="2"></el-option>
+            </el-select>
+            <el-select placeholder="选择应用" v-model="selectLineChannel" clearable style="width: 140px;">
+              <el-option label="教育行业Demo" value="1"></el-option>
+              <el-option label="游戏行业Demo" value="2"></el-option>
+              <el-option label="通用行业Demo" value="3"></el-option>
+            </el-select>
+            <el-select placeholder="操作人" v-model="selectLineChannel" clearable style="width: 140px;">
+              <el-option label="nina" value="1"></el-option>
+              <el-option label="luoyu" value="2"></el-option>
+              <el-option label="jcq" value="3"></el-option>
+              <el-option label="lwh" value="4"></el-option>
+            </el-select>
+          </div>
+          <el-table :height="500" :data="pagedTableRows" border>
+            <el-table-column prop="eventName" label="事件名" min-width="100" />
+            <el-table-column prop="eventIdentifier" label="事件标识符" min-width="140" />
+            <el-table-column label="计算状态" min-width="100">
+              <template #default="scope">
+                <el-tag :type="getStatusType(scope.row.status)">{{ scope.row.status }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="近30天事件上报数" min-width="100" align="center">
+              <template #default="scope">
+                <span v-if="scope.row.reportCount > 0">{{ scope.row.reportCount.toLocaleString() }}</span>
+                <span v-else>-</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="上报平台" min-width="100">
+              <template #default="scope">
+                {{ getPlatformText(scope.row.platforms) }}
+              </template>
+            </el-table-column>
+            <el-table-column label="应用" min-width="100">
+              <template #default="scope">
+                {{ getAppText(scope.row.apps) }}
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" min-width="200" fixed="right">
+              <template #default="scope">
+                <el-button @click="eventEdit(scope.row)" type="primary" link>编辑</el-button>
+                <el-button type="primary" link>详情</el-button>
+                <el-button v-if="scope.row.status === '计算中'" type="primary" link>暂停</el-button>
+                <el-button v-else-if="scope.row.status === '暂停计算'" type="primary" link>启用</el-button>
+                <el-button v-else type="primary" link>注册</el-button>
+                <el-button type="danger" link>删除</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+          <div class="flex justify-end mt-3">
+            <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" background
+              layout="total, prev, pager, next, sizes" :total="tableRows.length" :page-sizes="[5, 10, 20]" />
+          </div>
+        </div>
+      </Lcard>
+      <!-- 添加批量导入弹窗 -->
+      <BatchImportModal v-model="showBatchImportModal" @import="handleBatchImport" />
+      <!-- 添加事件弹窗 -->
+      <AddEventModal v-model="showAddEventModal" @add="handleAddEvent" />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" name="countMainTrend" setup>
+// import { ref, onMounted, watch, computed, defineAsyncComponent } from 'vue';
+import { useI18n } from 'vue-i18n';
+import * as echarts from 'echarts';
+// 引入组件
+const Lcard = defineAsyncComponent(() => import('/@/components/LYcom/Lcard/index.vue'));
+const Title = defineAsyncComponent(() => import('/@/components/Title/index.vue'));
+const { t } = useI18n();
+const eventTypeRadio = ref('1')
+
+onMounted(() => {
+  setTimeout(() => {
+  }, 500);
+});
+
+const emit = defineEmits(['update:eventId'])
+
+// 编辑事件
+const eventEdit = (data: any) => {
+  console.log(data);
+  emit('update:eventId', data.id)
+}
+
+// 导入批量导入弹窗组件
+const BatchImportModal = defineAsyncComponent(() => import('./BatchImportModal.vue'))
+
+// 控制批量导入弹窗显示
+const showBatchImportModal = ref(false)
+
+// 处理批量导入
+const handleBatchImport = (data: any) => {
+  console.log('批量导入数据:', data)
+  // 这里可以实现实际的导入逻辑
+  // 例如发送请求到后端处理Excel文件
+}
+
+// 导入添加事件弹窗组件
+const AddEventModal = defineAsyncComponent(() => import('./AddEventModal.vue'))
+
+// 控制添加事件弹窗显示
+const showAddEventModal = ref(false)
+
+// 处理添加事件
+const handleAddEvent = (data: any) => {
+  console.log('添加事件数据:', data)
+  // 这里可以实现实际的添加事件逻辑
+  // 例如发送请求到后端创建新事件
+}
+
+const searchData = reactive({
+  eventType: '1',
+  channel: '',
+  eventName: '',
+  eventHas: '',
+  appId: '',
+  evnetUser: '',
+})
+
+// 表格相关数据
+interface EventTableRow {
+  id: string;
+  eventName: string;
+  eventIdentifier: string;
+  status: string;
+  reportCount: number;
+  platforms: string[];
+  apps: string[];
+  operator: string;
+}
+
+// 表格相关(静态数据)
+const currentPage = ref(1);
+const pageSize = ref(10);
+
+const tableRows = ref<EventTableRow[]>([
+  {
+    id: '1',
+    eventName: '用户注册',
+    eventIdentifier: 'user_register',
+    status: '计算中',
+    reportCount: 12560,
+    platforms: ['Android', 'IOS'],
+    apps: ['教育行业Demo'],
+    operator: 'nina'
+  },
+  {
+    id: '2',
+    eventName: '用户登录',
+    eventIdentifier: 'user_login',
+    status: '计算中',
+    reportCount: 45210,
+    platforms: ['Android', 'IOS', 'Harmony'],
+    apps: ['教育行业Demo', '游戏行业Demo'],
+    operator: 'luoyu'
+  },
+  {
+    id: '3',
+    eventName: '商品浏览',
+    eventIdentifier: 'product_view',
+    status: '暂停计算',
+    reportCount: 0,
+    platforms: ['Android'],
+    apps: ['游戏行业Demo'],
+    operator: 'jcq'
+  },
+  {
+    id: '4',
+    eventName: '加入购物车',
+    eventIdentifier: 'add_to_cart',
+    status: '计算中',
+    reportCount: 8650,
+    platforms: ['Android', 'IOS'],
+    apps: ['游戏行业Demo'],
+    operator: 'lwh'
+  },
+  {
+    id: '5',
+    eventName: '完成支付',
+    eventIdentifier: 'payment_complete',
+    status: '计算中',
+    reportCount: 3240,
+    platforms: ['Android', 'IOS', 'Harmony'],
+    apps: ['教育行业Demo', '通用行业Demo'],
+    operator: 'nina'
+  },
+  {
+    id: '6',
+    eventName: '分享应用',
+    eventIdentifier: 'share_app',
+    status: '未注册',
+    reportCount: 0,
+    platforms: [],
+    apps: [],
+    operator: 'luoyu'
+  },
+  {
+    id: '7',
+    eventName: '查看个人资料',
+    eventIdentifier: 'view_profile',
+    status: '计算中',
+    reportCount: 15680,
+    platforms: ['Android', 'IOS'],
+    apps: ['教育行业Demo', '游戏行业Demo', '通用行业Demo'],
+    operator: 'jcq'
+  },
+  {
+    id: '8',
+    eventName: '搜索功能',
+    eventIdentifier: 'search_function',
+    status: '暂停计算',
+    reportCount: 0,
+    platforms: ['IOS'],
+    apps: ['教育行业Demo'],
+    operator: 'lwh'
+  },
+  {
+    id: '9',
+    eventName: '视频播放',
+    eventIdentifier: 'video_play',
+    status: '计算中',
+    reportCount: 27890,
+    platforms: ['Android', 'IOS'],
+    apps: ['教育行业Demo'],
+    operator: 'nina'
+  },
+  {
+    id: '10',
+    eventName: '收藏商品',
+    eventIdentifier: 'favorite_product',
+    status: '计算中',
+    reportCount: 5620,
+    platforms: ['Android', 'IOS', 'Harmony'],
+    apps: ['游戏行业Demo', '通用行业Demo'],
+    operator: 'luoyu'
+  },
+  {
+    id: '11',
+    eventName: '评论功能',
+    eventIdentifier: 'comment_function',
+    status: '未注册',
+    reportCount: 0,
+    platforms: [],
+    apps: [],
+    operator: 'jcq'
+  },
+  {
+    id: '12',
+    eventName: '消息推送点击',
+    eventIdentifier: 'push_notification_click',
+    status: '计算中',
+    reportCount: 9850,
+    platforms: ['Android', 'IOS'],
+    apps: ['教育行业Demo', '游戏行业Demo'],
+    operator: 'lwh'
+  },
+  {
+    id: '13',
+    eventName: '页面停留',
+    eventIdentifier: 'page_stay',
+    status: '计算中',
+    reportCount: 31560,
+    platforms: ['Android', 'Harmony'],
+    apps: ['通用行业Demo'],
+    operator: 'nina'
+  },
+  {
+    id: '14',
+    eventName: '应用启动',
+    eventIdentifier: 'app_launch',
+    status: '暂停计算',
+    reportCount: 0,
+    platforms: ['Android'],
+    apps: ['教育行业Demo'],
+    operator: 'luoyu'
+  },
+  {
+    id: '15',
+    eventName: '退出应用',
+    eventIdentifier: 'app_exit',
+    status: '未注册',
+    reportCount: 0,
+    platforms: [],
+    apps: [],
+    operator: 'jcq'
+  }
+]);
+
+const pagedTableRows = computed(() => {
+  const startIndex = (currentPage.value - 1) * pageSize.value;
+  return tableRows.value.slice(startIndex, startIndex + pageSize.value);
+});
+
+// 获取平台显示文本
+const getPlatformText = (platforms: string[]) => {
+  if (platforms.length === 0) return '-';
+  if (platforms.length > 2) return `${platforms[0]}等${platforms.length}个`;
+  return platforms.join('、');
+};
+
+// 获取应用显示文本
+const getAppText = (apps: string[]) => {
+  if (apps.length === 0) return '-';
+  if (apps.length > 2) return `${apps[0]}等${apps.length}个`;
+  return apps.join('、');
+};
+
+// 状态标签样式
+const getStatusType = (status: string) => {
+  switch (status) {
+    case '计算中':
+      return 'success';
+    case '暂停计算':
+      return 'warning';
+    case '未注册':
+      return 'info';
+    default:
+      return 'info';
+  }
+};
+</script>
+<style lang="scss" scoped>
+.search-box>div {
+  margin-right: 20px;
+}
+
+.msg {
+  height: 30px;
+  background-color: #f4f5fa;
+  line-height: 30px;
+  font-size: 12px;
+}
+
+.link {
+  color: #167af0;
+  cursor: pointer;
+}
+
+.box1 {
+  display: flex;
+  margin-top: 30px;
+
+  .card-box1,
+  .card-box2 {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: start;
+  }
+
+  .card-box1 {
+    .info-card {
+      width: 48%;
+    }
+  }
+
+  .card-box2 {
+    .info-card {
+      width: 33%;
+    }
+  }
+}
+
+.line {
+  margin: 60px -30px 30px;
+  height: 1px;
+  background-color: #E6E6E6;
+}
+</style>

+ 10 - 0
src/views/count/featureUsage/eventMannage/index.vue

@@ -0,0 +1,10 @@
+<template>
+  <EventTable v-if="!eventId" v-model:eventId="eventId"></EventTable>
+  <EventEdit v-else v-model:eventId="eventId"></EventEdit>
+</template>
+<script setup lang="ts" name="countMainTrend">
+const EventTable = defineAsyncComponent(() => import('./EventTable.vue'));
+const EventEdit = defineAsyncComponent(() => import('./EventEdit.vue'));
+
+const eventId = ref(null)
+</script>