wangxy пре 15 часа
родитељ
комит
57157747ac

+ 202 - 2
src/views/his/statistics/appOperationReport.vue

@@ -6,6 +6,9 @@
         <el-radio-button label="last">上月</el-radio-button>
       </el-radio-group>
       <span class="period-label">{{ periodLabel }}</span>
+      <el-select v-model="monthAppVersion" placeholder="APP 版本" clearable size="small" filterable style="margin-left: 20px; width: 150px;" @change="getData">
+        <el-option v-for="item in appVersionList" :key="item.versionId" :label="item.versionName" :value="item.versionCode" />
+      </el-select>
     </div>
 
     <el-row :gutter="20" class="card-row">
@@ -101,6 +104,104 @@
         </el-card>
       </el-col>
     </el-row>
+
+    <el-card shadow="hover" class="daily-report-card">
+      <div slot="header" class="card-header">
+        <span>日报表</span>
+      </div>
+      <el-form :inline="true" :model="dailyQueryParams" class="query-form">
+        <el-form-item label="时间范围">
+          <el-date-picker
+            v-model="dailyTimeRange"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            value-format="yyyy-MM-dd"
+            size="small"
+            style="width: 260px"
+            @change="handleDailyDateChange">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="APP 版本">
+          <el-select v-model="dailyQueryParams.appVersion" placeholder="请选择 APP 版本" clearable size="small" filterable>
+            <el-option v-for="item in appVersionList" :key="item.versionId" :label="item.versionName" :value="item.versionCode" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="getDailyReport">查询</el-button>
+        </el-form-item>
+      </el-form>
+      <el-table v-loading="dailyLoading" :data="dailyReportList" border style="width: 100%">
+        <el-table-column label="日期" align="center" prop="statDate" width="120" />
+        <el-table-column label="新增用户数" align="center" prop="newUsers" width="120" />
+        <el-table-column label="累计注册用户" align="center" prop="totalUsers" width="130" />
+        <el-table-column label="活跃用户数" align="center" prop="activeUsers" width="120" />
+        <el-table-column label="次日留存率(%)" align="center" prop="nextDayRetentionRate" width="130" />
+        <el-table-column label="平均使用时长(分钟)" align="center" prop="avgUseDuration" width="150" />
+        <el-table-column label="LTV(元)" align="center" prop="ltv" width="100" />
+        <el-table-column label="CAC(元)" align="center" prop="cac" width="100" />
+        <el-table-column label="日流失率(%)" align="center" prop="dailyChurnRate" width="110" />
+      </el-table>
+      <pagination
+        v-show="dailyTotal > 0"
+        :total="dailyTotal"
+        :page.sync="dailyQueryParams.pageNum"
+        :limit.sync="dailyQueryParams.pageSize"
+        @pagination="getDailyReport"
+      />
+    </el-card>
+
+    <el-card shadow="hover" class="weekly-report-card">
+      <div slot="header" class="card-header">
+        <span>周报表</span>
+      </div>
+      <el-form :inline="true" :model="weeklyQueryParams" class="query-form">
+        <el-form-item label="时间范围">
+          <el-date-picker
+            v-model="weeklyTimeRange"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            value-format="yyyy-MM-dd"
+            size="small"
+            style="width: 260px"
+            @change="handleWeeklyDateChange">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="APP 版本">
+          <el-select v-model="weeklyQueryParams.appVersion" placeholder="请选择 APP 版本" clearable size="small" filterable>
+            <el-option v-for="item in appVersionList" :key="item.versionId" :label="item.versionName" :value="item.versionCode" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="getWeeklyReport">查询</el-button>
+        </el-form-item>
+      </el-form>
+      <el-table v-loading="weeklyLoading" :data="weeklyReportList" border style="width: 100%">
+        <el-table-column label="周期" align="center" width="180">
+          <template slot-scope="scope">
+            第{{ scope.$index + 1 + (weeklyQueryParams.pageNum - 1) * weeklyQueryParams.pageSize }}周 ({{ scope.row.weekStart }} ~ {{ scope.row.weekEnd }})
+          </template>
+        </el-table-column>
+        <el-table-column label="新增用户数" align="center" prop="newUsers" width="120" />
+        <el-table-column label="累计注册用户" align="center" prop="totalUsers" width="130" />
+        <el-table-column label="活跃用户数" align="center" prop="activeUsers" width="120" />
+        <el-table-column label="留存率(%)" align="center" prop="retentionRate" width="110" />
+        <el-table-column label="平均使用时长(分钟)" align="center" prop="avgUseDuration" width="150" />
+        <el-table-column label="LTV(元)" align="center" prop="ltv" width="100" />
+        <el-table-column label="CAC(元)" align="center" prop="cac" width="100" />
+        <el-table-column label="周流失率(%)" align="center" prop="weeklyChurnRate" width="110" />
+      </el-table>
+      <pagination
+        v-show="weeklyTotal > 0"
+        :total="weeklyTotal"
+        :page.sync="weeklyQueryParams.pageNum"
+        :limit.sync="weeklyQueryParams.pageSize"
+        @pagination="getWeeklyReport"
+      />
+    </el-card>
   </div>
 </template>
 
@@ -114,7 +215,31 @@ export default {
       periodType: "current",
       reportData: {},
       year: null,
-      month: null
+      month: null,
+      monthAppVersion: null,
+      appVersionList: [],
+      dailyTimeRange: null,
+      dailyLoading: false,
+      dailyReportList: [],
+      dailyTotal: 0,
+      dailyQueryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        startDate: null,
+        endDate: null,
+        appVersion: null
+      },
+      weeklyTimeRange: null,
+      weeklyLoading: false,
+      weeklyReportList: [],
+      weeklyTotal: 0,
+      weeklyQueryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        startDate: null,
+        endDate: null,
+        appVersion: null
+      }
     };
   },
   computed: {
@@ -124,6 +249,7 @@ export default {
   },
   created() {
     this.initPeriod();
+    this.getAppVersionList();
     this.getData();
   },
   methods: {
@@ -148,11 +274,72 @@ export default {
         method: 'get',
         params: {
           year: this.year,
-          month: this.month
+          month: this.month,
+          appVersion: this.monthAppVersion
         }
       }).then(response => {
         this.reportData = response.data || {};
       });
+    },
+    getAppVersionList() {
+      request({
+        url: '/his/appVersion/appVersions',
+        method: 'get'
+      }).then(response => {
+        this.appVersionList = response.data || [];
+      });
+    },
+    handleDailyDateChange() {
+      if (this.dailyTimeRange && this.dailyTimeRange.length === 2) {
+        this.dailyQueryParams.startDate = this.dailyTimeRange[0];
+        this.dailyQueryParams.endDate = this.dailyTimeRange[1];
+      } else {
+        this.dailyQueryParams.startDate = null;
+        this.dailyQueryParams.endDate = null;
+      }
+    },
+    getDailyReport() {
+      if (!this.dailyQueryParams.startDate || !this.dailyQueryParams.endDate) {
+        this.$message.warning('请选择时间范围');
+        return;
+      }
+      this.dailyLoading = true;
+      request({
+        url: '/his/appOperationReport/dailyReport',
+        method: 'get',
+        params: this.dailyQueryParams
+      }).then(response => {
+        this.dailyReportList = response.rows || [];
+        this.dailyTotal = response.total || 0;
+      }).finally(() => {
+        this.dailyLoading = false;
+      });
+    },
+    handleWeeklyDateChange() {
+      if (this.weeklyTimeRange && this.weeklyTimeRange.length === 2) {
+        this.weeklyQueryParams.startDate = this.weeklyTimeRange[0];
+        this.weeklyQueryParams.endDate = this.weeklyTimeRange[1];
+      } else {
+        this.weeklyQueryParams.startDate = null;
+        this.weeklyQueryParams.endDate = null;
+      }
+    },
+    getWeeklyReport() {
+      if (!this.weeklyQueryParams.startDate || !this.weeklyQueryParams.endDate) {
+        this.$message.warning('请选择时间范围');
+        return;
+      }
+      this.weeklyLoading = true;
+      request({
+        url: '/his/appOperationReport/weeklyReport',
+        method: 'get',
+        params: this.weeklyQueryParams
+      }).then(response => {
+        this.weeklyReportList = response.rows || [];
+        this.weeklyTotal = response.total || 0;
+      }).finally(() => {
+        this.weeklyLoading = false;
+      });
     }
   }
 };
@@ -212,4 +399,17 @@ export default {
   color: #909399;
   margin-top: 6px;
 }
+.daily-report-card {
+  margin-top: 20px;
+}
+.card-header {
+  font-size: 16px;
+  font-weight: bold;
+}
+.query-form {
+  margin-bottom: 15px;
+}
+.weekly-report-card {
+  margin-top: 20px;
+}
 </style>

+ 144 - 0
src/views/his/statistics/userActiveRetention.vue

@@ -0,0 +1,144 @@
+<template>
+  <div class="app-container">
+    <el-card shadow="hover">
+      <div slot="header" class="card-header">
+        <span>用户活跃留存表</span>
+      </div>
+      <el-form :inline="true" :model="queryParams" class="query-form">
+        <el-form-item label="时间范围">
+          <el-date-picker
+            v-model="timeRange"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            value-format="yyyy-MM-dd"
+            size="small"
+            style="width: 260px"
+            @change="handleDateChange">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="用户类型">
+          <el-select v-model="queryParams.userType" placeholder="请选择用户类型" clearable size="small">
+            <el-option label="新用户" :value="1" />
+            <el-option label="老用户" :value="2" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="APP 版本">
+          <el-select v-model="queryParams.appVersion" placeholder="请选择 APP 版本" clearable size="small" filterable>
+            <el-option v-for="item in appVersionList" :key="item.versionId" :label="item.versionName" :value="item.versionCode" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="getList">查询</el-button>
+        </el-form-item>
+      </el-form>
+      <el-table v-loading="loading" :data="dataList" border style="width: 100%">
+        <el-table-column label="日期" align="center" prop="statDate" width="120" fixed />
+        <el-table-column label="用户日均启动次数" align="center">
+          <el-table-column label="1次" align="center" prop="launchCount1" width="80" />
+          <el-table-column label="2-3次" align="center" prop="launchCount23" width="80" />
+          <el-table-column label="4次+" align="center" prop="launchCount4Plus" width="80" />
+        </el-table-column>
+        <el-table-column label="使用时长分布" align="center">
+          <el-table-column label="<30分钟" align="center" prop="durationLt30" width="90" />
+          <el-table-column label="30分钟-1小时" align="center" prop="duration30To60" width="110" />
+          <el-table-column label="1-2小时" align="center" prop="duration60To120" width="90" />
+          <el-table-column label="2-4小时" align="center" prop="duration120To240" width="90" />
+          <el-table-column label="4小时+" align="center" prop="durationGt240" width="80" />
+        </el-table-column>
+        <el-table-column label="平均访问间隔(天)" align="center" prop="avgVisitInterval" width="120" />
+        <el-table-column label="沉默用户唤醒率(%)" align="center" prop="silentWakeUpRate" width="130" />
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import request from '@/utils/request'
+
+export default {
+  name: "UserActiveRetention",
+  data() {
+    return {
+      timeRange: null,
+      loading: false,
+      dataList: [],
+      appVersionList: [],
+      queryParams: {
+        startDate: null,
+        endDate: null,
+        userType: null,
+        appVersion: null
+      }
+    };
+  },
+  created() {
+    this.initDefaultDate();
+    this.getAppVersionList();
+  },
+  methods: {
+    initDefaultDate() {
+      const now = new Date();
+      const year = now.getFullYear();
+      const month = String(now.getMonth() + 1).padStart(2, '0');
+      const day = String(now.getDate()).padStart(2, '0');
+      const today = `${year}-${month}-${day}`;
+      this.timeRange = [today, today];
+      this.queryParams.startDate = today;
+      this.queryParams.endDate = today;
+    },
+    handleDateChange() {
+      if (this.timeRange && this.timeRange.length === 2) {
+        this.queryParams.startDate = this.timeRange[0];
+        this.queryParams.endDate = this.timeRange[1];
+      } else {
+        this.queryParams.startDate = null;
+        this.queryParams.endDate = null;
+      }
+    },
+    getAppVersionList() {
+      request({
+        url: '/his/appVersion/appVersions',
+        method: 'get'
+      }).then(response => {
+        this.appVersionList = response.data || [];
+      });
+    },
+    getList() {
+      if (!this.queryParams.startDate || !this.queryParams.endDate) {
+        this.$message.warning('请选择时间范围');
+        return;
+      }
+      const start = new Date(this.queryParams.startDate);
+      const end = new Date(this.queryParams.endDate);
+      const diffTime = Math.abs(end - start);
+      const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+      if (diffDays > 31) {
+        this.$message.warning('时间范围最多选择一个月');
+        return;
+      }
+      this.loading = true;
+      request({
+        url: '/his/userBehavior/daily',
+        method: 'get',
+        params: this.queryParams
+      }).then(response => {
+        this.dataList = response.data || [];
+      }).finally(() => {
+        this.loading = false;
+      });
+    }
+  }
+};
+</script>
+
+<style scoped>
+.card-header {
+  font-size: 16px;
+  font-weight: bold;
+}
+.query-form {
+  margin-bottom: 15px;
+}
+</style>