Parcourir la source

feat:卸载模块接口联调及组件构建

cmy il y a 1 jour
Parent
commit
120081dfe5

+ 1 - 0
.env

@@ -5,6 +5,7 @@ VITE_IS_MICRO= true
 VITE_PUBLIC_PATH = /
 
 # 后端请求前缀
+# VITE_API_URL = http://192.168.3.118:9999
 VITE_API_URL = http://192.168.10.101:9999
 # VITE_API_URL = http://192.168.3.17:9999
 

+ 91 - 0
src/api/count/churn.ts

@@ -34,4 +34,95 @@ export const uninstallPortrait = (data?: Object) => {
 		method: 'post',
 		data: data,
 	});
+};
+
+/**
+ * 卸载归因-卸载预测
+ * @param cycle	周期(周/月)
+ * @param type	类型(0-卸载流失设备, 1-召回设备(预测))	
+ * @param appId	应用ID	
+ * @param channel	渠道	
+ * @param version	应用版本	
+ * @param startDate	开始日期	
+ * @param endDate	结束日期	
+ * @returns 
+ */
+export const uninstallPredict = (data?: Object) => {
+	return request({
+		url: '/stats/uninstall/predict',
+		method: 'post',
+		data: data,
+	});
+};
+
+/**
+ * 卸载归因-安装卸载比
+ * @param cycle	周期(周/月)
+ * @param appId	应用ID	
+ * @param channel	渠道	
+ * @param version	应用版本	
+ * @param startDate	开始日期	
+ * @param endDate	结束日期	
+ * @param pageNum	页码
+ * @param pageSize	每页大小
+ * @returns 
+ */
+export const predictDetail = (data?: Object) => {
+	return request({
+		url: '/stats/uninstall/predict/detail',
+		method: 'post',
+		data: data,
+	});
+};
+
+/**
+ * 卸载归因-安装卸载比明细
+ * @param cycle	周期(周/月)
+ * @param appId	应用ID	
+ * @param channel	渠道	
+ * @param version	应用版本	
+ * @param pageNum	页码
+ * @param pageSize	每页大小	
+ * @returns 
+ */
+export const predictDetailList = (data?: Object) => {
+	return request({
+		url: '/stats/uninstall/predict/detailList',
+		method: 'post',
+		data: data,
+	});
+};
+
+/**
+ * 卸载洞察-体验干扰
+ * @param cycle	周期(周/月)
+ * @param type	类型
+ * @param appId	应用ID	
+ * @param channel	渠道	
+ * @param version	应用版本	
+ * @returns 
+ */
+export const uninstallInterfere = (data?: Object) => {
+	return request({
+		url: '/stats/uninstall/uninstallInterfere',
+		method: 'post',
+		data: data,
+	});
+};
+
+/**
+ * 卸载洞察-行为还原
+ * @param cycle	周期(周/月)
+ * @param type	类型
+ * @param appId	应用ID	
+ * @param channel	渠道	
+ * @param version	应用版本	
+ * @returns 
+ */
+export const uninstallAction = (data?: Object) => {
+	return request({
+		url: '/stats/uninstall/uninstallAction',
+		method: 'post',
+		data: data,
+	});
 };

+ 4 - 1
src/layout/navMenu/horizontal.vue

@@ -5,7 +5,10 @@
 				<template v-for="val in menuLists">
 					<el-menu-item v-if="val.children && val.children.length > 0" :index="val.path" :key="val.path">
 						<template #title>
-							<SvgIcon :name="val.meta.icon" />
+              <navIcon v-if="val.path == '/marketing'" :val=val style="margin-right: 8px;" />
+              <navIcon v-else-if="val.path == '/count'" :val=val style="margin-right: 8px;" />
+              <navIcon v-else-if="val.path == '/system'" :val=val style="margin-right: 8px;" />
+							<SvgIcon v-else :name="val.meta.icon" />
 							{{ other.setMenuI18n(val) }}
 						</template>
 					</el-menu-item>

+ 31 - 6
src/layout/navMenu/navIcon.vue

@@ -20,6 +20,19 @@
       d="M12 7C12.4741 7 12.9326 7.06712 13.3672 7.19043C13.3733 7.25701 13.3794 7.32381 13.3857 7.39062C12.2996 8.47255 12.2318 8.54261 10.9326 9.8418C10.9171 9.85727 10.7679 9.99874 10.6426 10.1572C10.6405 10.1599 10.6388 10.1634 10.6367 10.166C10.6341 10.1693 10.6315 10.1724 10.6289 10.1758C10.4322 10.4278 10.2895 10.7172 10.2119 11.0264L10.1826 11.1641C10.1141 11.532 10.136 11.9115 10.248 12.2686C10.3602 12.6256 10.5587 12.9501 10.8252 13.2129C11.0916 13.4755 11.4189 13.6684 11.7773 13.7754C12.2267 13.9103 12.6984 13.9132 13.1514 13.7617C13.5821 13.6176 13.9038 13.3649 14.1318 13.1357C15.4417 11.8226 15.5442 11.7215 16.6309 10.6309C16.6914 10.6365 16.7524 10.6418 16.8135 10.6475C16.9341 11.0778 17 11.5312 17 12C17 14.7614 14.7614 17 12 17C9.23858 17 7 14.7614 7 12C7 9.23858 9.23858 7 12 7Z"
       fill="white" />
   </svg>
+  <svg v-else-if="val.path == '/count'" style="margin-right: 8px;" width="24" height="24" viewBox="0 0 24 24" fill="none"
+    xmlns="http://www.w3.org/2000/svg">
+    <mask id="mask0_821_3166" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
+      <rect width="24" height="24" fill="#D9D9D9" />
+    </mask>
+    <g mask="url(#mask0_821_3166)">
+      <rect opacity="0.4" x="2.5" y="2.7998" width="19" height="14.7143" rx="1.5" fill="#ffffff" stroke="#ffffff" />
+      <path d="M5.80957 12.2994L10.0953 8.72796L12.9524 11.5851L17.2381 8.01367" stroke="#ffffff" stroke-width="2"
+        stroke-linecap="round" stroke-linejoin="round" />
+      <line x1="5.85742" y1="20.8232" x2="18.1431" y2="20.8232" stroke="#ffffff" stroke-width="2"
+        stroke-linecap="round" />
+    </g>
+  </svg>
   <svg v-else-if="val.path == '/system'" style="margin-right: 8px;" width="24" height="24" viewBox="0 0 24 24"
     fill="none" xmlns="http://www.w3.org/2000/svg">
     <path opacity="0.4"
@@ -29,8 +42,18 @@
       d="M12.005 16C14.2141 16 16.005 14.2091 16.005 12C16.005 9.79086 14.2141 8 12.005 8C9.79587 8 8.005 9.79086 8.005 12C8.005 14.2091 9.79587 16 12.005 16Z"
       fill="white" />
   </svg>
-  <svg v-if="val.path == '/count/churn'" class="active-icon" style="margin-left: 2px; margin-right: 8px;" width="16" height="16"
-    viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <svg v-if="val.path == '/count/device'" class="active-icon" style="margin-left: 3px; margin-right: 10px;" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path d="M14.0002 2.66699H2.00016C1.63197 2.66699 1.3335 2.96547 1.3335 3.33366V12.667C1.3335 13.0352 1.63197 13.3337 2.00016 13.3337H14.0002C14.3684 13.3337 14.6668 13.0352 14.6668 12.667V3.33366C14.6668 2.96547 14.3684 2.66699 14.0002 2.66699Z" stroke="#167AF0" stroke-linejoin="round"/>
+    <path d="M4 6L6.33333 8L4 10" stroke="#167AF0" stroke-linecap="round" stroke-linejoin="round"/>
+    <path d="M7.6665 10.667H11.9998" stroke="#167AF0" stroke-linecap="round" stroke-linejoin="round"/>
+  </svg>
+  <svg v-if="val.path == '/count/device'" class="default-icon" style="margin-left: 3px; margin-right: 10px;" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path d="M14.0002 2.66699H2.00016C1.63197 2.66699 1.3335 2.96547 1.3335 3.33366V12.667C1.3335 13.0352 1.63197 13.3337 2.00016 13.3337H14.0002C14.3684 13.3337 14.6668 13.0352 14.6668 12.667V3.33366C14.6668 2.96547 14.3684 2.66699 14.0002 2.66699Z" stroke="#646464" stroke-linejoin="round"/>
+    <path d="M4 6L6.33333 8L4 10" stroke="#646464" stroke-linecap="round" stroke-linejoin="round"/>
+    <path d="M7.6665 10.667H11.9998" stroke="#646464" stroke-linecap="round" stroke-linejoin="round"/>
+  </svg>
+  <svg v-if="val.path == '/count/churn'" class="active-icon" style="margin-left: 3px; margin-right: 10px;" width="16"
+    height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
     <path d="M2.66675 1.33301H13.3334" stroke="#167AF0" stroke-linecap="round" stroke-linejoin="round" />
     <path d="M2.66675 14.667H13.3334" stroke="#167AF0" stroke-linecap="round" stroke-linejoin="round" />
     <path d="M4 1.33301V5.33301L7 8.66634" stroke="#167AF0" stroke-linecap="round" stroke-linejoin="round" />
@@ -42,8 +65,8 @@
     <path d="M9.71558 10.8818L9.95128 11.1175" stroke="#167AF0" stroke-linecap="round" stroke-linejoin="round" />
     <path d="M8 12.667H8.33333" stroke="#167AF0" stroke-linecap="round" stroke-linejoin="round" />
   </svg>
-  <svg v-if="val.path == '/count/churn'" class="default-icon" style="margin-left: 2px; margin-right: 8px;" width="16" height="16"
-    viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <svg v-if="val.path == '/count/churn'" class="default-icon" style="margin-left: 3px; margin-right: 10px;" width="16"
+    height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
     <path d="M2.66675 1.33301H13.3334" stroke="#646464" stroke-linecap="round" stroke-linejoin="round" />
     <path d="M2.66675 14.667H13.3334" stroke="#646464" stroke-linecap="round" stroke-linejoin="round" />
     <path d="M4 1.33301V5.33301L7 8.66634" stroke="#646464" stroke-linecap="round" stroke-linejoin="round" />
@@ -55,7 +78,8 @@
     <path d="M9.71558 10.8818L9.95128 11.1175" stroke="#646464" stroke-linecap="round" stroke-linejoin="round" />
     <path d="M8 12.667H8.33333" stroke="#646464" stroke-linecap="round" stroke-linejoin="round" />
   </svg>
-  <svg v-if="val.path == '/count/engagement'" class="active-icon" style="margin-left: 2px; margin-right: 8px;" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <svg v-if="val.path == '/count/engagement'" class="active-icon" style="margin-left: 3px; margin-right: 10px;"
+    width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
     <path d="M8.15 13.44L12.44 9.15L11.01 10.58L9.58 12.01L8.15 13.44ZM8.15 13.44L1 6.29L5.29 2L8.15 4.86"
       stroke="#167AF0" stroke-linecap="round" stroke-linejoin="round" />
     <path
@@ -64,7 +88,8 @@
     <path d="M9.58007 12.01L8.50757 10.9375" stroke="#167AF0" stroke-linecap="round" stroke-linejoin="round" />
     <path d="M11.01 10.5803L9.9375 9.50781" stroke="#167AF0" stroke-linecap="round" stroke-linejoin="round" />
   </svg>
-  <svg v-if="val.path == '/count/engagement'" class="default-icon" style="margin-left: 2px; margin-right: 8px;" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <svg v-if="val.path == '/count/engagement'" class="default-icon" style="margin-left: 3px; margin-right: 10px;"
+    width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
     <path d="M8.15 13.44L12.44 9.15L11.01 10.58L9.58 12.01L8.15 13.44ZM8.15 13.44L1 6.29L5.29 2L8.15 4.86"
       stroke="#646464" stroke-linecap="round" stroke-linejoin="round" />
     <path

+ 1 - 0
src/layout/navMenu/vertical.vue

@@ -12,6 +12,7 @@
 				<template #title>
           <navIcon v-if="val.path == '/count/churn'" :val=val style="margin-right: 8px;" />
           <navIcon v-else-if="val.path == '/count/engagement'" :val=val style="margin-right: 8px;" />
+          <navIcon v-else-if="val.path == '/count/device'" :val=val style="margin-right: 8px;" />
           <SvgIcon v-else :name="val.meta.icon" />
 					<span>{{ other.setMenuI18n(val) }}</span>
 				</template>

+ 9 - 2
src/theme/app.scss

@@ -395,9 +395,16 @@ body,
 	.el-menu-item:hover svg,
 	.el-sub-menu.is-active svg,
 	.el-sub-menu:hover svg {
-	    path {
-	        fill: var(--menu-bar-active-font-color) !important;
+    path {
+        fill: var(--menu-bar-active-font-color) !important;
+        stroke: var(--menu-bar-active-font-color) !important;
 		}
+    line {
+      stroke: var(--menu-bar-active-font-color) !important;
+    }
+    rect {
+      fill: var(--menu-bar-active-font-color) !important;
+    }
 	}
 }
 

+ 130 - 88
src/views/count/churn/ascribe/index.vue

@@ -3,41 +3,41 @@
     <div class="ascribe">
       <el-row :gutter="12" style="padding: 0 12px 12px; row-gap: 12px;">
         <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
-          <el-card shadow="none" style="padding: 10px 14px;">
-            <div class="top-info">
-              <div class="title">卸载归因<el-tooltip class="box-item" effect="light" content="" placement="right-start">
-                  <svg style="margin: 0 0 0 8px; vertical-align: baseline;" width="14" height="14" viewBox="0 0 14 14"
-                    fill="none" xmlns="http://www.w3.org/2000/svg">
-                    <path
-                      d="M7 14C8.93298 14 10.683 13.2165 11.9497 11.9497C13.2165 10.683 14 8.93298 14 7C14 5.06702 13.2165 3.31702 11.9497 2.05025C10.683 0.783503 8.93298 0 7 0C5.06702 0 3.31702 0.783503 2.05025 2.05025C0.783503 3.31702 0 5.06702 0 7C0 8.93298 0.783503 10.683 2.05025 11.9497C3.31702 13.2165 5.06702 14 7 14Z"
-                      fill="#1B4D88" fill-opacity="0.4" />
-                    <path
-                      d="M4 4.702C4 4.40333 4.02333 4.09533 4.07 3.778C4.126 3.46067 4.21933 3.17133 4.35 2.91C4.49 2.64867 4.67667 2.434 4.91 2.266C5.14333 2.08867 5.45133 2 5.834 2H7.794C8.102 2 8.37267 2.06533 8.606 2.196C8.84867 2.31733 9.04467 2.476 9.194 2.672C9.35267 2.868 9.474 3.092 9.558 3.344C9.65133 3.596 9.70733 3.848 9.726 4.1C9.754 4.352 9.74467 4.59467 9.698 4.828C9.66067 5.06133 9.59533 5.26667 9.502 5.444L7.934 8.314V9.574H6.324V8.146L7.808 5.556C7.892 5.416 7.948 5.234 7.976 5.01C8.01333 4.786 8.01333 4.57133 7.976 4.366C7.948 4.15133 7.878 3.96933 7.766 3.82C7.66333 3.67067 7.514 3.596 7.318 3.596H6.408C6.24933 3.596 6.11867 3.624 6.016 3.68C5.91333 3.72667 5.82933 3.80133 5.764 3.904C5.708 3.99733 5.67067 4.114 5.652 4.254C5.63333 4.38467 5.624 4.534 5.624 4.702H4ZM7.976 12.15H6.324V10.512H7.976V12.15Z"
-                      fill="white" />
-                  </svg>
-                  <template #content>
-                    <div style="width: 300px;">
-                      卸载归因解读卸载设备在您应用中的最后活跃行为。
-                      本模块功能展示周期内的卸载设备,在卸载前的最后7天(含当天)在您的应用中浏览次数TOP10的页面;展示应用在全网设备中的卸载量,及是否在当前周期内新安装了您关注行业的头部竞品。同时基于行业提供各周期的应用安装卸载比,辅助您判断行业的规模趋势。
-                    </div>
-                  </template>
-                </el-tooltip>
-              </div>
-
+          <LayoutHeader title="卸载归因" :style="{ marginBottom: '0' }">
+            <template #aside>
               <div class="data-source-status">数据源状态:Demo数据</div>
-            </div>
-          </el-card>
+            </template>
+            <template #tooltip-content>
+              卸载归因解读卸载设备在您应用中的最后活跃行为。<br/>
+              本模块功能展示周期内的卸载设备,在卸载前的最后7天(含当天)在您的应用中浏览次数TOP10的页面;<br/>
+              展示应用在全网设备中的卸载量,及是否在当前周期内新安装了您关注行业的头部竞品。<br/>
+              同时基于行业提供各周期的应用安装卸载比,辅助您判断行业的规模趋势。
+            </template>
+            <template #content>
+            </template>
+          </LayoutHeader>
         </el-col>
       </el-row>
-
       <el-row :gutter="12" style="padding: 0 12px 12px; row-gap: 12px;">
         <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
           <el-card shadow="none">
             <div class="trend-container">
-              <div class="title">卸载设备全量预测</div>
+              <div class="title">卸载设备全量预测<svg
+                width="24" height="22" viewBox="0 0 24 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+                <path d="M0 13C0 5.8203 5.8203 0 13 0H22C23.1046 0 24 0.895431 24 2V9C24 16.1797 18.1797 22 11 22H2C0.89543 22 0 21.1046 0 20V13Z" fill="url(#paint0_linear_834_812)"/>
+                <path d="M6 16.5L9.63695 5.5H11.6343L15.2712 16.5H13.4379L12.5137 13.3806H8.6979L7.77376 16.5H6ZM9.11526 12.0075H12.0964L11.6641 10.5299C11.3064 9.33582 10.9784 8.11194 10.6356 6.87313H10.576C10.2481 8.12687 9.90525 9.33582 9.54752 10.5299L9.11526 12.0075Z" fill="white"/>
+                <path d="M16.271 16.5V5.5H18V16.5H16.271Z" fill="white"/>
+                <defs>
+                <linearGradient id="paint0_linear_834_812" x1="12" y1="0" x2="12" y2="22" gradientUnits="userSpaceOnUse">
+                <stop stop-color="#4CFFEA"/>
+                <stop offset="1" stop-color="#0081EB"/>
+                </linearGradient>
+                </defs>
+                </svg>
+              </div>
               <div class="card-tabs">
-                <div class="card-tab" :class="{ active: activeTab === 'churnTrend' }"
-                  @click="handleTabClick('churnTrend')">
+                <div class="card-tab" :class="{ active: activeTab === Type.CHURN }"
+                  @click="handleTabClick(Type.CHURN)">
                   卸载流失设备(预测)
                   <el-tooltip effect="light" content="" placement="right-start">
                     <svg width="14" height="14" viewBox="0 0 14 14" fill="none"
@@ -67,8 +67,8 @@
                   </el-tooltip>
 
                 </div>
-                <div class="card-tab" :class="{ active: activeTab === 'recallTrend' }"
-                  @click="handleTabClick('recallTrend')">卸载召回设备(预测)
+                <div class="card-tab" :class="{ active: activeTab === Type.RECALL }"
+                  @click="handleTabClick(Type.RECALL)">卸载召回设备(预测)
                   <el-tooltip effect="light" content="" placement="right-start">
                     <svg width="14" height="14" viewBox="0 0 14 14" fill="none"
                       xmlns="http://www.w3.org/2000/svg">
@@ -100,7 +100,7 @@
               <!-- 折线图 -->
               <div class="chart-container">
                 <LineChart :data="currentChartData" :color="'#167af0'" height="270px"
-                  :smooth="false" :area-style="true" :title="activeTab === 'churnTrend' ? '卸载流失设备' : '卸载召回设备'" :showLegend="true" />
+                  :smooth="false" :area-style="true" :title="activeTab === Type.CHURN ? '卸载流失设备' : '卸载召回设备'" :showLegend="true" />
               </div>
             </div>
           </el-card>
@@ -115,13 +115,13 @@
               <!-- 折线图 -->
               <div class="chart-container">
                 <LineChart :data="data" :color="'#167af0'" height="270px" :showLegend="true"
-                  :smooth="false" :area-style="true" :isMultiSeries="true" :seriesNames="['当前应用', '移动视频行业TOP5', '医疗服务行业TOP5']" />
+                  :smooth="false" :area-style="true" :isMultiSeries="true" :seriesNames="seriesNames" />
               </div>
               
               <el-divider style="margin: 30px 0; background: rgba(230, 230, 230, 1);"/>
 
               <!-- 表格 -->
-              <ExportToCSV :hidePage="true" :data="formatData" :columns="columns" :fileName="''" :hide-table="false" :tableStyle="{minWidth: '1476px'}" />
+              <ExportToCSV :data="formatData" :columns="columns" :fileName="''" :hide-table="false" :tableStyle="{maxWidth: '1476px'}" />
             </div>
           </el-card>
         </el-col>
@@ -134,75 +134,117 @@
 import LineChart from '/@/views/count/components/LineChart.vue'
 import ExportToCSV from '/@/views/count/components/ExportToCSV.vue';
 import { ref, computed } from 'vue'
+import LayoutHeader from '/@/views/count/components/LayoutHeader.vue';
+import { uninstallPredict, predictDetail } from '/@/api/count/churn';
+import { formatDate } from '/@/utils/formatTime';
+
+// 类型枚举
+enum Type {
+  CHURN = 0,
+  RECALL = 1
+}
 
-// 流失趋势数据
-const churnTrendData = ref([
-  { date: '2025-01-01', value: 25 },
-  { date: '2025-01-02', value: 32 },
-  { date: '2025-01-03', value: 28 },
-  { date: '2025-01-04', value: 45 },
-  { date: '2025-01-05', value: 38 },
-  { date: '2025-01-06', value: 42 },
-  { date: '2025-01-07', value: 35 },
-  { date: '2025-01-08', value: 48 },
-  { date: '2025-01-09', value: 52 },
-  { date: '2025-01-10', value: 39 },
-  { date: '2025-01-11', value: 44 },
-  { date: '2025-01-12', value: 37 },
-  { date: '2025-01-13', value: 41 },
-  { date: '2025-01-14', value: 46 }
-])
-
-// 召回趋势数据
-const recallTrendData = ref([
-  { date: '2025-01-01', value: 15 },
-  { date: '2025-01-02', value: 22 },
-  { date: '2025-01-03', value: 18 },
-  { date: '2025-01-04', value: 35 },
-  { date: '2025-01-05', value: 28 },
-  { date: '2025-01-06', value: 32 },
-  { date: '2025-01-07', value: 25 },
-  { date: '2025-01-08', value: 38 },
-  { date: '2025-01-09', value: 42 },
-  { date: '2025-01-10', value: 29 },
-  { date: '2025-01-11', value: 34 },
-  { date: '2025-01-12', value: 27 },
-  { date: '2025-01-13', value: 31 },
-  { date: '2025-01-14', value: 36 }
-])
-
-const activeTab = ref('churnTrend')
-
-// 计算当前图表数据
-const currentChartData = computed(() => {
-  return activeTab.value === 'churnTrend' ? churnTrendData.value : recallTrendData.value
-})
+const activeTab = ref(Type.CHURN);
+const currentChartData = ref([] as any[])
 
-const handleTabClick = (tab: string) => {
+const handleTabClick = (tab: number) => {
   activeTab.value = tab
+  getUninstallPredict();
 }
 
 const data = ref([
-  { date: '2025-01-01', values: [{ value: 1, seriesName: '当前应用' }, { value: 2, seriesName: '移动视频行业TOP5' }, { value: 5, seriesName: '医疗服务行业TOP5' }] },
-  { date: '2025-01-02', values: [{ value: 11, seriesName: '当前应用' }, { value: 4, seriesName: '移动视频行业TOP5' }, { value: 3, seriesName: '医疗服务行业TOP5' }] },
-  { date: '2025-01-03', values: [{ value: 2, seriesName: '当前应用' }, { value: 2, seriesName: '移动视频行业TOP5' }, { value: 3, seriesName: '医疗服务行业TOP5' }] },
-  { date: '2025-01-04', values: [{ value: 11, seriesName: '当前应用' }, { value: 5, seriesName: '移动视频行业TOP5' }, { value: 9, seriesName: '医疗服务行业TOP5' }] }
+  // { date: '2025-01-01', values: [{ value: 1, seriesName: '当前应用' }, { value: 2, seriesName: '移动视频行业TOP5' }, { value: 5, seriesName: '医疗服务行业TOP5' }] },
 ])
 
 const columns = ref([
   {prop: 'date', label: '日期'},
-  {prop: 'app1', label: '当前应用'},
-  {prop: 'app2', label: '移动视频行业TOP5'},
-  {prop: 'app3', label: '医疗服务行业TOP5'},
+  // {prop: 'app1', label: '当前应用'},
 ])
 
+const seriesNames = computed(() => {
+  return columns.value.filter((item: any) => item.prop !== 'date').map((item: any) => item.label)
+})
+
 const formatData = computed(() => {
-  return data.value.map((item: any) => ({
-    date: item.date,
-    app1: item.values[0].value,
-    app2: item.values[1].value,
-    app3: item.values[2].value
-  }))
+  return data.value.map((item: any) => {
+    const formattedItem: any = {
+      date: item.date
+    };
+    
+    // 动态添加每个系列的数据
+    item.values.forEach((valueItem: any, index: number) => {
+      const propName = `app${index + 1}`;
+      formattedItem[propName] = valueItem.value;
+    });
+    
+    return formattedItem;
+  })
+})
+
+const getUninstallPredict = (data?: any) => {
+  uninstallPredict({
+    type: activeTab.value,
+    cycle: 'week',
+    ...data
+  }).then((res) => {
+    if(res.code === 0 && res.data?.length > 0) {
+      currentChartData.value = res.data.map((item: any) => ({
+        date: formatDate(new Date(item.predictTime), 'YYYY-mm-dd'),
+        value: activeTab.value === Type.CHURN ? item.uninstallCount : item.recallCount
+      }))
+    }
+  })
+}
+
+const getPredictDetail = (params?: any) => {
+  predictDetail({
+    cycle: 'week',
+    ...params
+  }).then((res) => {
+    console.log(res);
+    const newColumns: any[] = []
+    if(res.code === 0 && res.data?.length > 0) {
+      columns.value = [];
+      data.value = res.data.map((item: any, index: number) => {
+        let rowData = {
+          date: formatDate(new Date(item.date), 'YYYY-mm-dd'),
+          values: [] as {value: number, seriesName: string}[]
+        }
+
+        item.type.forEach((type: any, _index: number) => {
+          const [key, _value] = Object.entries(type)[0];
+          console.log(key, _value);
+          rowData.values.push({
+            value: _value as number,
+            seriesName: key
+          })
+          newColumns.push({
+            prop: 'app' + (1+_index),
+            label: key
+          })
+        })
+
+        if(index === 0) {
+          columns.value = [
+            {prop: 'date', label: '日期'},
+            ...newColumns
+          ];
+        }
+
+        console.log(columns.value);
+        return rowData;
+      })
+      data.value = data.value.sort((a: any, b: any) => {
+        return new Date(a.date).getTime() - new Date(b.date).getTime()
+      })
+      console.log(data.value);
+    }
+  })
+}
+
+onMounted(() => {
+  getUninstallPredict();
+  getPredictDetail();
 })
 </script>
 <style scoped lang="scss">

+ 72 - 24
src/views/count/churn/behavior/components/BeforeUninstallStatus.vue

@@ -35,8 +35,8 @@
               <p>卸载前体验干扰</p>
             </div>
             <div class="card-tabs">
-              <div class="card-tab" :class="{ active: last7DaysTab === 'unloadExperience' }" @click="last7DaysTab = 'unloadExperience'">前7天崩溃次数</div>
-              <div class="card-tab" :class="{ active: last7DaysTab === 'pushReceiptLast7Days' }" @click="last7DaysTab = 'pushReceiptLast7Days'">前7天推送接收</div>
+              <div class="card-tab" :class="{ active: last7DaysTab === Last7DaysTab.CRASH }" @click="handleLast7DaysTabClick(Last7DaysTab.CRASH)">前7天崩溃次数</div>
+              <div class="card-tab" :class="{ active: last7DaysTab === Last7DaysTab.PUSH }" @click="handleLast7DaysTabClick(Last7DaysTab.PUSH)">前7天推送接收</div>
             </div>
             <div class="content-item">
               <BarChart :title="'卸载设备数'" :data="experienceData" />
@@ -49,11 +49,11 @@
               <p>卸载前行为还原</p>
             </div>
             <div class="card-tabs">
-              <div class="card-tab" :class="{ active: viewPagesTab === 'highFrequencyPages' }" @click="viewPagesTab = 'highFrequencyPages'">高频浏览页面TOP10</div>
-              <div class="card-tab" :class="{ active: viewPagesTab === 'finalViewPages' }" @click="viewPagesTab = 'finalViewPages'">最终浏览页面TOP10</div>
+              <div class="card-tab" :class="{ active: viewPagesTab === ViewPagesTab.HIGH_FREQUENCY_PAGES }" @click="handleViewPagesTabClick(ViewPagesTab.HIGH_FREQUENCY_PAGES)">高频浏览页面TOP10</div>
+              <div class="card-tab" :class="{ active: viewPagesTab === ViewPagesTab.FINAL_VIEW_PAGES }" @click="handleViewPagesTabClick(ViewPagesTab.FINAL_VIEW_PAGES)">最终浏览页面TOP10</div>
             </div>
             <div class="content-item">
-              <HorizontalBarChart :data="chartData2" :title="['页面名称', '触发次数']" />
+              <HorizontalBarChart :data="ViewPagesData" :title="['页面名称', '触发次数']" />
             </div>
           </div>
         </el-col>
@@ -65,6 +65,7 @@
 <script lang="ts" setup>
 import BarChart from '/@/views/count/components/BarChart.vue'
 import HorizontalBarChart from './HorizontalBarChart.vue'
+import { uninstallAction, uninstallInterfere } from '/@/api/count/churn'
 
 const chartData = [
   { name: '0-7天', value: 45, percentage: '84.00%' },
@@ -85,27 +86,74 @@ const usageCountData = [
   { name: '6次以上', value: 3, percentage: '3.0%' }
 ]
 
-const experienceData = [
-  { name: '0次', value: 45, percentage: '45.0%' },
-  { name: '1次', value: 23, percentage: '23.0%' },
-  { name: '2次', value: 15, percentage: '15.0%' },
-  { name: '3次', value: 8, percentage: '8.0%' },
-  { name: '4次', value: 6, percentage: '6.0%' },
-  { name: '5次以上', value: 3, percentage: '3.0%' }
-]
+const experienceData = ref([] as any[])
+const ViewPagesData = ref([] as any[])
 
-const chartData2 = [
-  { name: '首页首页首页首页', value: 45},
-  { name: '首页首页首页', value: 25},
-  { name: '首页首页首页', value: 43},
-  { name: '首页首页首页', value: 46},
-  { name: '首页首页首页', value: 45},
-  { name: '首页首页首页', value: 12},
-  { name: '首页首页首页', value: 76}
-]
+enum Last7DaysTab {
+  CRASH = '1',
+  PUSH = '2'
+}
+
+enum ViewPagesTab {
+  HIGH_FREQUENCY_PAGES = '1',
+  FINAL_VIEW_PAGES = '2'
+}
+
+const last7DaysTab = ref(Last7DaysTab.CRASH);
+const viewPagesTab = ref(ViewPagesTab.HIGH_FREQUENCY_PAGES);
+
+const getUninstallInterfere = () => {
+  uninstallInterfere({
+    cycle: 'week',
+    type: last7DaysTab.value,
+  }).then((res) => {
+    if(res.code === 0 && res.data?.value?.length > 0) {
+      experienceData.value = []
+      res.data.value.forEach((item: any) => {
+        const [key, val] = Object.entries(item)[0];
+        experienceData.value.push({
+          name: key,
+          value: val
+        })
+      })
+    }
+  })
+}
+
+const getUninstallAction = () => {
+  uninstallAction({
+    cycle: 'week',
+    type: viewPagesTab.value,
+  }).then((res) => {
+    if(res.code === 0 && res.data?.value?.length > 0) {
+      ViewPagesData.value = []
+      res.data.value.forEach((item: any) => {
+        const [key, val] = Object.entries(item)[0];
+        ViewPagesData.value.push({
+          name: key,
+          value: val
+        })
+      })
+      ViewPagesData.value.sort((a: any, b: any) => b.value - a.value)
+    }
+  })
+}
+
+const handleLast7DaysTabClick = (tab: Last7DaysTab) => {
+  last7DaysTab.value = tab
+  getUninstallInterfere()
+}
+
+const handleViewPagesTabClick = (tab: ViewPagesTab) => {
+  viewPagesTab.value = tab
+  getUninstallAction()
+}
+
+onMounted(() => {
+  getUninstallInterfere();
+  getUninstallAction();
+})
 
-const last7DaysTab = ref('unloadExperience');
-const viewPagesTab = ref('highFrequencyPages');
 </script>
 
 <style scoped lang="scss">

+ 8 - 8
src/views/count/churn/behavior/components/HorizontalBarChart.vue

@@ -20,14 +20,14 @@
     <!-- 鼠标跟随浮窗 -->
     <Transition name="tooltip-fade" mode="out-in">
       <div v-if="tooltipVisible" 
-           class="tooltip" 
-           :class="{ 'tooltip-left': shouldShowOnLeft }"
-           :style="{ 
-             left: tooltipPosition.x + 'px', 
-             top: tooltipPosition.y + 'px',
-             opacity: tooltipOpacity
-           }"
-           v-html="tooltipContent">
+        class="tooltip" 
+        :class="{ 'tooltip-left': shouldShowOnLeft }"
+        :style="{ 
+          left: tooltipPosition.x + 'px', 
+          top: tooltipPosition.y + 'px',
+          opacity: tooltipOpacity
+        }"
+        v-html="tooltipContent">
       </div>
     </Transition>
   </div>

+ 12 - 22
src/views/count/churn/behavior/index.vue

@@ -3,29 +3,18 @@
     <div class="behavior">
       <el-row :gutter="12" style="padding: 0 12px 12px; row-gap: 12px;">
         <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
-          <el-card shadow="none" style="padding: 10px 14px;">
-            <div class="top-info">
-              <div class="title">卸载洞察<el-tooltip class="box-item" effect="light"
-                  content="" placement="right-start">
-                  <svg style="margin: 0 0 0 8px; vertical-align: baseline;" width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-                    <path
-                      d="M7 14C8.93298 14 10.683 13.2165 11.9497 11.9497C13.2165 10.683 14 8.93298 14 7C14 5.06702 13.2165 3.31702 11.9497 2.05025C10.683 0.783503 8.93298 0 7 0C5.06702 0 3.31702 0.783503 2.05025 2.05025C0.783503 3.31702 0 5.06702 0 7C0 8.93298 0.783503 10.683 2.05025 11.9497C3.31702 13.2165 5.06702 14 7 14Z"
-                      fill="#1B4D88" fill-opacity="0.4" />
-                    <path
-                      d="M4 4.702C4 4.40333 4.02333 4.09533 4.07 3.778C4.126 3.46067 4.21933 3.17133 4.35 2.91C4.49 2.64867 4.67667 2.434 4.91 2.266C5.14333 2.08867 5.45133 2 5.834 2H7.794C8.102 2 8.37267 2.06533 8.606 2.196C8.84867 2.31733 9.04467 2.476 9.194 2.672C9.35267 2.868 9.474 3.092 9.558 3.344C9.65133 3.596 9.70733 3.848 9.726 4.1C9.754 4.352 9.74467 4.59467 9.698 4.828C9.66067 5.06133 9.59533 5.26667 9.502 5.444L7.934 8.314V9.574H6.324V8.146L7.808 5.556C7.892 5.416 7.948 5.234 7.976 5.01C8.01333 4.786 8.01333 4.57133 7.976 4.366C7.948 4.15133 7.878 3.96933 7.766 3.82C7.66333 3.67067 7.514 3.596 7.318 3.596H6.408C6.24933 3.596 6.11867 3.624 6.016 3.68C5.91333 3.72667 5.82933 3.80133 5.764 3.904C5.708 3.99733 5.67067 4.114 5.652 4.254C5.63333 4.38467 5.624 4.534 5.624 4.702H4ZM7.976 12.15H6.324V10.512H7.976V12.15Z"
-                      fill="white" />
-                  </svg>
-                  <template #content>
-                    <div style="width: 300px;">
-                      卸载洞察展示您每周卸载设备的活跃特征,如从安装到卸载的生命周期时长分布、卸载前活跃情况、末次活跃至卸载行为的时间差分布、卸载设备终端特征。
-                    </div>
-                  </template>
-                </el-tooltip>
+          <LayoutHeader title="卸载洞察" :style="{ marginBottom: '0' }">
+            <template #aside>
+              <div class="data-source-status">数据源状态: Demo数据</div>
+            </template>
+            <template #tooltip-content>
+              卸载洞察展示您每周卸载设备的活跃特征,如从安装到卸载的生命周期时长分布、卸载前活跃情况、末次活跃至卸载行为的时间差分布、卸载设备终端特征。
+            </template>
+            <template #content>
+              <div class="content">
               </div>
-
-              <div class="data-source-status">数据源状态:Demo数据</div>
-            </div>
-          </el-card>
+            </template>
+          </LayoutHeader>
         </el-col>
       </el-row>
       <el-row :gutter="12" style="padding: 0 12px 12px; row-gap: 12px;">
@@ -91,6 +80,7 @@ import BeforeUninstallStatus from './components/BeforeUninstallStatus.vue'
 import AfterUninstallStatus from './components/AfterUninstallStatus.vue'
 import SystemDistribution from './components/SystemDistribution.vue'
 import PieChart from './components/PieChart.vue'
+import LayoutHeader from '/@/views/count/components/LayoutHeader.vue'
 
 const historyUninstallData = [
   { name: '1次', value: 45, percentage: '45.0%' },

+ 1 - 1
src/views/count/churn/overview/ProgressCard.vue

@@ -15,7 +15,7 @@
         <div class="content-item-title">{{ title }}</div>
         <div class="content-item-value">{{ counts }}</div>
         <div class="content-item-percent">环比<span :style="{ color: color }">{{ rates }}%</span><svg 
-          v-if="rates < 0" width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+          v-if="rates <= 0" width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
             <mask id="mask0_611_555" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="14"
               height="14">
               <rect width="14" height="14" fill="#D9D9D9" />

+ 12 - 21
src/views/count/churn/overview/index.vue

@@ -3,26 +3,17 @@
     <div class="overview">
       <el-row :gutter="12" style="padding: 0 12px 12px; row-gap: 12px;">
         <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
-          <el-card shadow="none" style="padding: 10px 14px;">
-            <div class="top-info">
-              <div class="title">流失概况</div>
-              <div class="aside">
-                <div class="data-source-status">数据源状态: Demo数据</div>
-                <el-button class="goto-smart-operation" type="primary" link>前往智能运营发短信
-                  <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-                    <path d="M10.6667 4.66699L13.3334 7.33366L10.6667 10.0003" stroke="#167AF0" stroke-width="1.5"
-                      stroke-linecap="round" stroke-linejoin="round" />
-                    <path d="M2.66671 12.6663V8.33301C2.66671 7.78071 3.11441 7.33301 3.66671 7.33301H13.3334"
-                      stroke="#167AF0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
-                  </svg>
-                </el-button>
+          <LayoutHeader title="流失概况">
+            <template #aside>
+              <div class="data-source-status">数据源状态: Demo数据</div>
+            </template>
+            <template #content>
+              <div class="top-content">
+                <ProgressCard :title="'当周卸载流失设备数'" :counts="trendProgressData.uninstallCounts" :rates="trendProgressData.uninstallRates" />
+                <ProgressCard :title="'当周卸载召回设备数'" :counts="trendProgressData.recallCounts" :rates="trendProgressData.recallRates" />
               </div>
-            </div>
-            <div class="top-content">
-              <ProgressCard :title="'当周卸载流失设备数'" :counts="trendProgressData.uninstallCounts" :rates="trendProgressData.uninstallRates" />
-              <ProgressCard :title="'当周卸载召回设备数'" :counts="trendProgressData.recallCounts" :rates="trendProgressData.recallRates" />
-            </div>
-          </el-card>
+            </template>
+          </LayoutHeader>
         </el-col>
       </el-row>
       <el-row :gutter="12" style="padding: 0 12px 12px; row-gap: 12px;">
@@ -78,12 +69,12 @@
 
 <script lang="ts" name="churnOverview" setup>
 import LineChart from '/@/views/count/components/LineChart.vue'
-import ProgressRing from '/@/views/count/components/ProgressRing.vue'
 import { BasicTableProps, useTable } from '/@/hooks/table';
 import { ref, computed, reactive } from 'vue'
 import { uninstallTrend } from '/@/api/count/churn'
 import { formatDate } from '/@/utils/formatTime';
 import ProgressCard from './ProgressCard.vue'
+import LayoutHeader from '/@/views/count/components/LayoutHeader.vue'
 
 const cellStyle = ref({
   textAlign: 'center',
@@ -246,7 +237,7 @@ svg {
     font-size: 14px;
     line-height: 20px;
     color: rgba(18, 18, 18, 1);
-    padding: 4px 16px 4px 0;
+    padding: 4px 0 4px 0;
   }
 
   .goto-smart-operation {

+ 1 - 1
src/views/count/churn/portrait/components/UserInfo.vue

@@ -30,7 +30,7 @@
         </svg>
       </div>
       <div class="title">{{ title }}</div>
-      <div class="tags" style="min-height: 20px;">
+      <div class="tags" style="min-height: 23px;">
         <div class="tag" v-for="tag in tags" :key="tag">{{ tag }}</div>
       </div>
     </div>

+ 61 - 48
src/views/count/churn/portrait/index.vue

@@ -2,46 +2,42 @@
   <div class="layout-padding">
     <el-row :gutter="12" style="padding: 0 12px 12px; row-gap: 12px;">
       <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
-        <el-card class="select-form-card" shadow="none" style="padding: 10px 14px">
-          <div class="card-title no-padding">卸载画像<el-tooltip class="box-item" effect="light" content="敬请期待" placement="right-start">
-              <svg style="margin: 0 0 0 8px; vertical-align: baseline;" width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-                <path
-                  d="M7 14C8.93298 14 10.683 13.2165 11.9497 11.9497C13.2165 10.683 14 8.93298 14 7C14 5.06702 13.2165 3.31702 11.9497 2.05025C10.683 0.783503 8.93298 0 7 0C5.06702 0 3.31702 0.783503 2.05025 2.05025C0.783503 3.31702 0 5.06702 0 7C0 8.93298 0.783503 10.683 2.05025 11.9497C3.31702 13.2165 5.06702 14 7 14Z"
-                  fill="#1B4D88" fill-opacity="0.4" />
-                <path
-                  d="M4 4.702C4 4.40333 4.02333 4.09533 4.07 3.778C4.126 3.46067 4.21933 3.17133 4.35 2.91C4.49 2.64867 4.67667 2.434 4.91 2.266C5.14333 2.08867 5.45133 2 5.834 2H7.794C8.102 2 8.37267 2.06533 8.606 2.196C8.84867 2.31733 9.04467 2.476 9.194 2.672C9.35267 2.868 9.474 3.092 9.558 3.344C9.65133 3.596 9.70733 3.848 9.726 4.1C9.754 4.352 9.74467 4.59467 9.698 4.828C9.66067 5.06133 9.59533 5.26667 9.502 5.444L7.934 8.314V9.574H6.324V8.146L7.808 5.556C7.892 5.416 7.948 5.234 7.976 5.01C8.01333 4.786 8.01333 4.57133 7.976 4.366C7.948 4.15133 7.878 3.96933 7.766 3.82C7.66333 3.67067 7.514 3.596 7.318 3.596H6.408C6.24933 3.596 6.11867 3.624 6.016 3.68C5.91333 3.72667 5.82933 3.80133 5.764 3.904C5.708 3.99733 5.67067 4.114 5.652 4.254C5.63333 4.38467 5.624 4.534 5.624 4.702H4ZM7.976 12.15H6.324V10.512H7.976V12.15Z"
-                  fill="white" />
-              </svg>
-            </el-tooltip>
-          </div>
-          <div class="select-form">
-            <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
-              <path d="M33 15H15V30H33V15Z" fill="#167AF0" stroke="#167AF0" stroke-width="2" stroke-linejoin="round" />
-              <path d="M20 35L24 30L28 35" stroke="#167AF0" stroke-width="2" stroke-linecap="round"
-                stroke-linejoin="round" />
-              <path d="M18 25L21.0967 22.034L23.5345 24.3738L29 19" stroke="white" stroke-width="2"
-                stroke-linecap="round" stroke-linejoin="round" />
-              <path d="M13 15H35" stroke="#167AF0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
-              <rect width="48" height="48" rx="14" fill="#167AF0" fill-opacity="0.1" />
-            </svg>
-            <el-form :inline="true" :model="form" label-width="0">
-              <el-form-item label="">
-                <el-select v-model="form.time" placeholder="" @change="selectTimeChange">
-                  <el-option label="按周查看" value="week" />
-                  <el-option label="按月查看" value="month" />
-                </el-select>
-              </el-form-item>
-              <el-form-item label="">
-                <el-date-picker :type="'daterange'" style="width: 242px;" v-model="form.dateArray" range-separator="至" @change="selectDateChange"
-                  start-placeholder="开始日期" end-placeholder="结束日期" />
-              </el-form-item>
-              <el-form-item label="">
-                一周内,卸载流失设备数<span>{{ uninstallCount.count }}</span>
-              </el-form-item>
-              <div class="tips">您可以根据下方的卸载画像报表,查看易卸载用户和高粘性用户的特征,了解什么样的用户留不住。</div>
-            </el-form>
-          </div>
-        </el-card>
+        <LayoutHeader title="卸载画像" :style="{ marginBottom: '0' }">
+          <template #tooltip-content>
+            敬请期待
+          </template>
+          <template #content>
+            <div class="content">
+              <div class="select-form">
+                <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+                  <path d="M33 15H15V30H33V15Z" fill="#167AF0" stroke="#167AF0" stroke-width="2" stroke-linejoin="round" />
+                  <path d="M20 35L24 30L28 35" stroke="#167AF0" stroke-width="2" stroke-linecap="round"
+                    stroke-linejoin="round" />
+                  <path d="M18 25L21.0967 22.034L23.5345 24.3738L29 19" stroke="white" stroke-width="2"
+                    stroke-linecap="round" stroke-linejoin="round" />
+                  <path d="M13 15H35" stroke="#167AF0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
+                  <rect width="48" height="48" rx="14" fill="#167AF0" fill-opacity="0.1" />
+                </svg>
+                <el-form :inline="true" :model="form" label-width="0">
+                  <el-form-item label="">
+                    <el-select v-model="form.time" placeholder="" @change="selectTimeChange">
+                      <el-option label="按周查看" value="week" />
+                      <el-option label="按月查看" value="month" />
+                    </el-select>
+                  </el-form-item>
+                  <el-form-item label="">
+                    <el-date-picker disabled :type="'daterange'" style="width: 242px;" v-model="form.dateArray" range-separator="至" @change="selectDateChange"
+                      start-placeholder="开始日期" end-placeholder="结束日期" :disabled-date="disabledDate" />
+                  </el-form-item>
+                  <el-form-item label="">
+                    {{ form.time == 'week' ? '一周内' : '一月内' }},卸载流失设备数<span>{{ uninstallCount.count }}</span>
+                  </el-form-item>
+                  <div class="tips">您可以根据下方的卸载画像报表,查看易卸载用户和高粘性用户的特征,了解什么样的用户留不住。</div>
+                </el-form>
+              </div>
+            </div>
+          </template>
+        </LayoutHeader>
       </el-col>
     </el-row>
 
@@ -51,14 +47,14 @@
           <el-row :gutter="12" style="padding: 0 12px 0; row-gap: 12px;">
             <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
               <el-card shadow="none">
-                <user-info title="不易卸载用户画像" :type="true" :tags="hardData.length > 0 ? hardData.map((item: any) => item.name) : []" tips="卸载用户 vs. 活跃用户具备的明显特征,有以下特征的用户易卸载" style="margin-bottom: 24px;"></user-info>
-                <data-table :data="hardData" :columns="columns"></data-table>
+                <UserInfo title="易卸载用户特征" :type="true" :tags="easyTags" tips="卸载用户 vs. 活跃用户具备的明显特征,有以下特征的用户易卸载" style="margin-bottom: 24px;"></UserInfo>
+                <DataTable :data="easyData" :columns="columns"></DataTable>
               </el-card>
             </el-col>
             <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
               <el-card shadow="none">
-                <user-info title="不易卸载用户特征" :type="false" :tags="easyData.length > 0 ? easyData.map((item: any) => item.name) : []" tips="活跃用户 vs. 卸载用户具备的明显差异点,有以下特征的人群卸载可能性低" style="margin-bottom: 24px;"></user-info>
-                <data-table :data="easyData" :columns="columns"></data-table>
+                <UserInfo title="不易卸载用户特征" :type="false" :tags="hardTags" tips="活跃用户 vs. 卸载用户具备的明显差异点,有以下特征的人群卸载可能性低" style="margin-bottom: 24px;"></UserInfo>
+                <DataTable :data="hardData" :columns="columns"></DataTable>
               </el-card>
             </el-col>
           </el-row>
@@ -91,12 +87,12 @@
             <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
               <div style="padding-top: 8px;" v-if="echartsData.length > 0">
                 <BarChart :data="echartsData" :is-multi-series="true" :series-names="seriesNames" title=""
-                  style="height: 320px;" :chartHeight="320" />
+                  style="height: 380px;" :chartHeight="380" />
               </div>
             </el-col>
             <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
               <div class="">
-                <data-table :data="usageTimeData" :columns="columns2"></data-table>
+                <DataTable :data="usageTimeData" :columns="columns2"></DataTable>
               </div>
             </el-col>
           </el-row>
@@ -106,12 +102,13 @@
   </div>
 </template>
 <script setup lang="ts">
-import { ref } from 'vue';
+import { ref, onMounted } from 'vue';
 import UserInfo from './components/UserInfo.vue';
 import DataTable from './components/DataTable.vue';
 import BarChart from '/@/views/count/components/BarChart.vue';
 import { formatDate } from '/@/utils/formatTime';
 import { uninstallPortrait } from '/@/api/count/churn';
+import LayoutHeader from '/@/views/count/components/LayoutHeader.vue';
 
 const columns = ref([
   { label: '用户特征', prop: 'name', width: '100px' },
@@ -129,7 +126,7 @@ const columns2 = ref([
 
 const form = ref({
   time: 'week',
-  dateArray: [formatDate(new Date(new Date().setDate(new Date().getDate() - 7)), 'YYYY-mm-dd'), formatDate(new Date(), 'YYYY-mm-dd')]
+  dateArray: [formatDate(new Date(new Date().setDate(new Date().getDate() - 15)), 'YYYY-mm-dd'), formatDate(new Date(new Date().setDate(new Date().getDate() - 9)), 'YYYY-mm-dd')]
 })
 
 const echartsData = ref<any[]>([])
@@ -140,6 +137,8 @@ const usageTimeData = ref<any[]>([])// 使用次数
 const uninstallCount = ref({
   count: 0,
 })
+const hardTags = ref<string[]>([])
+const easyTags = ref<string[]>([])
 
 const init = () => {
   uninstallPortrait({
@@ -154,6 +153,9 @@ const init = () => {
       const newData: any[] = [];
       easyData.value = [];
       hardData.value = [];
+      easyTags.value = [];
+      hardTags.value = [];
+
       usageTimeData.value = [];
       res.data?.uninstallPortraitPhaseList.forEach((item: any) => {
         newData.push({
@@ -181,11 +183,13 @@ const init = () => {
             ...item,
             percent: item.percent.substring(0, item.percent.length - 1),
           });
+          easyTags.value = [...easyData.value].sort((a: any, b: any) => b.percent - a.percent).map((item: any) => item.name).slice(0, 3);
         } else {
           hardData.value.push({
             ...item,
             percent: item.percent.substring(0, item.percent.length - 1),
           });
+          hardTags.value = [...hardData.value].sort((a: any, b: any) => b.percent - a.percent).map((item: any) => item.name).slice(0, 3);
         }
       })
     // }
@@ -194,6 +198,11 @@ const init = () => {
 
 const selectTimeChange = () => {
   console.log(form.value.time);
+  if(form.value.time == 'week'){
+    form.value.dateArray = [formatDate(new Date(new Date().setDate(new Date().getDate() - 15)), 'YYYY-mm-dd'), formatDate(new Date(new Date().setDate(new Date().getDate() - 9)), 'YYYY-mm-dd')]
+  } else {
+    form.value.dateArray = [formatDate(new Date(new Date().setDate(new Date().getDate() - 39)), 'YYYY-mm-dd'), formatDate(new Date(new Date().setDate(new Date().getDate() - 9)), 'YYYY-mm-dd')]
+  }
   init();
 }
 
@@ -202,6 +211,10 @@ const selectDateChange = () => {
   init();
 }
 
+const disabledDate = (time: Date) => {
+  return time.getTime() > Date.now() - 86400000*9; 
+}
+
 onMounted(() => {
   init();
 })

+ 1 - 6
src/views/count/components/BarChart.vue

@@ -59,11 +59,6 @@ const props = withDefaults(defineProps<{
 
 // 默认数据
 const defaultData: BarItem[] = [
-  // { name: '6次以上', value: 45, percentage: '45.0%' },
-  // { name: '5次', value: 23, percentage: '23.0%' },
-  // { name: '4次', value: 15, percentage: '15.0%' },
-  // { name: '3次', value: 8, percentage: '8.0%' },
-  // { name: '2次', value: 6, percentage: '6.0%' },
   // { name: '1次', value: 3, percentage: '3.0%' }
 ]
 
@@ -187,7 +182,7 @@ const chartOption = computed(() => {
         // size: 包含 contentSize(tooltip内容区域的大小)和 viewSize(可视区域的大小)
         
         // 示例:将tooltip显示在鼠标上方
-        return [point[0], point[1] - 10];
+        // return [point[0], point[1] - 10];
         
         // 其他位置选项:
         // return 'top'; // 固定在顶部

+ 42 - 43
src/views/count/components/ExportToCSV.vue

@@ -9,29 +9,28 @@
         </svg>
       </div>
 
-      <div v-if="fileName" class="export-button" @click="handleExportData(dataList, columns, fileName)">导出
+      <div v-show="fileName" class="export-button" @click="handleExportData(dataList, columns, fileName)">导出
         <span style="vertical-align: middle; margin: 0 0 0 8px;">
           <Svg name="export2"></Svg>
         </span>
       </div>
 
-        <div style="overflow-x: auto;">
-          <el-table class="table" :data="paginatedData" row-key="name" style="width: 100%"
-            :cell-style="cellStyle" :header-cell-style="headerCellStyle" v-if="!hideTable">
-            <el-table-column v-for="(column, index) in columns" :key="column.prop" :label="column.label"
-              :prop="column.prop" show-overflow-tooltip :formatter="statusFormatter">
-              <template #default="scope">
-                <!-- 省市列特殊处理 -->
-                <span v-if="column.prop === 'province'" 
-                      class="province-link" 
-                      @click="handleProvinceClick(scope.row[column.prop])">
-                  {{ scope.row[column.prop] }}
-                </span>
-                <!-- 其他列正常显示 -->
-                <span v-else>{{ scope.row[column.prop] }}</span>
-              </template>
-            </el-table-column>
-          </el-table>
+      <div style="overflow-x: auto; width: 100%; display: block;">
+        <el-table class="table" :data="paginatedData" row-key="name" style="width: 100%" fit 
+          :cell-style="cellStyle" :header-cell-style="headerCellStyle" v-show="!hideTable">
+          <el-table-column v-for="(column, index) in columns" :key="column.prop" :label="column.label"
+            :prop="column.prop" show-overflow-tooltip :formatter="statusFormatter">
+            <template #default="scope">
+              <!-- 可点击 -->
+              <span v-show="column.type === 'link'" class="link" 
+                @click="column.handleClick ? column.handleClick(scope.row[column.prop]) : null">
+                {{ scope.row[column.prop] }}
+              </span>
+              <!-- 其他列 -->
+              <span v-show="column.type !== 'link'">{{ scope.row[column.prop] }}</span>
+            </template>
+          </el-table-column>
+        </el-table>
       </div>
 
       <pagination v-if="!hideTable && !hidePage" @current-change="handleCurrentChange" @size-change="handleSizeChange"
@@ -70,7 +69,12 @@ const props = defineProps({
     default: () => []
   },
   columns: {
-    type: Array as PropType<{ prop: string; label: string }[]>,
+    type: Array as PropType<{ 
+      prop: string;
+      label: string;
+      type?: string;
+      handleClick?: (row: any) => void | undefined
+    }[]>,
     default: () => []
   },
   fileName: {
@@ -99,11 +103,6 @@ const props = defineProps({
   }
 })
 
-// 定义 emit 事件
-const emit = defineEmits<{
-  provinceClick: [province: string]
-}>()
-
 // 表格数据
 const dataList = ref(props.data);
 const hideTable = ref(props.hideTable);
@@ -154,25 +153,21 @@ const statusFormatter = (row: any, column: any, cellValue: any, index: any) => {
   return cellValue || '--';
 }
 
-// 处理省市点击事件
-const handleProvinceClick = (province: string) => {
-  emit('provinceClick', province);
-}
-
 const handleExportData = (data: any, columns: any, fileName: string) => {
-  try {
-    // 检查是否有数据
-    if (!data || data.length === 0) {
-      useMessage().warning('没有数据可导出');
-      return;
-    }
-
-    const exportData = formatTableDataForExport(data, columns);
-    exportToExcel(exportData, fileName);
-  } catch (error) {
-    console.error('导出失败:', error);
-    useMessage().error('导出失败,请检查数据格式');
-  }
+  alert('导出');
+  // try {
+  //   // 检查是否有数据
+  //   if (!data || data.length === 0) {
+  //     useMessage().warning('没有数据可导出');
+  //     return;
+  //   }
+
+  //   const exportData = formatTableDataForExport(data, columns);
+  //   exportToExcel(exportData, fileName);
+  // } catch (error) {
+  //   console.error('导出失败:', error);
+  //   useMessage().error('导出失败,请检查数据格式');
+  // }
 }
 
 </script>
@@ -204,7 +199,7 @@ svg {
   }
 }
 
-.province-link {
+.link {
   color: rgba(22, 122, 240, 1);
   cursor: pointer;
   &:hover {
@@ -216,4 +211,8 @@ svg {
 :deep(.el-date-editor--dates .el-input__wrapper) {
   cursor: pointer;
 }
+
+:deep(.el-table) {
+  table-layout: fixed;
+}
 </style>

+ 0 - 1
src/views/count/components/HorizontalBarChart.vue

@@ -198,7 +198,6 @@ const chartOption = computed(() => {
         // return [10, 10]; // 固定坐标位置
       },
       formatter: function (params: any) {
-        console.log(params);
         return params.map((item: any) => {
           return `<i style="display: inline-block; width: 10px; height: 10px; background-color:rgba(22, 122, 240, 1); border-radius: 50%;"></i>
            ${item.name}: <span style="color:rgba(22, 122, 240, 1);">${item.value}</span>`

+ 79 - 0
src/views/count/components/LayoutHeader.vue

@@ -0,0 +1,79 @@
+<template>
+  <el-card shadow="none" :style="computedCardStyle">
+    <div class="top-info" :style="style">
+      <div class="title">{{ title }}<slot v-if="tooltip" name="tooltip">
+          <el-tooltip class="box-item" effect="light"
+            content="" placement="right-start">
+            <svg style="margin: 0 0 0 8px; vertical-align: baseline;" width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <path
+                d="M7 14C8.93298 14 10.683 13.2165 11.9497 11.9497C13.2165 10.683 14 8.93298 14 7C14 5.06702 13.2165 3.31702 11.9497 2.05025C10.683 0.783503 8.93298 0 7 0C5.06702 0 3.31702 0.783503 2.05025 2.05025C0.783503 3.31702 0 5.06702 0 7C0 8.93298 0.783503 10.683 2.05025 11.9497C3.31702 13.2165 5.06702 14 7 14Z"
+                fill="#1B4D88" fill-opacity="0.4" />
+              <path
+                d="M4 4.702C4 4.40333 4.02333 4.09533 4.07 3.778C4.126 3.46067 4.21933 3.17133 4.35 2.91C4.49 2.64867 4.67667 2.434 4.91 2.266C5.14333 2.08867 5.45133 2 5.834 2H7.794C8.102 2 8.37267 2.06533 8.606 2.196C8.84867 2.31733 9.04467 2.476 9.194 2.672C9.35267 2.868 9.474 3.092 9.558 3.344C9.65133 3.596 9.70733 3.848 9.726 4.1C9.754 4.352 9.74467 4.59467 9.698 4.828C9.66067 5.06133 9.59533 5.26667 9.502 5.444L7.934 8.314V9.574H6.324V8.146L7.808 5.556C7.892 5.416 7.948 5.234 7.976 5.01C8.01333 4.786 8.01333 4.57133 7.976 4.366C7.948 4.15133 7.878 3.96933 7.766 3.82C7.66333 3.67067 7.514 3.596 7.318 3.596H6.408C6.24933 3.596 6.11867 3.624 6.016 3.68C5.91333 3.72667 5.82933 3.80133 5.764 3.904C5.708 3.99733 5.67067 4.114 5.652 4.254C5.63333 4.38467 5.624 4.534 5.624 4.702H4ZM7.976 12.15H6.324V10.512H7.976V12.15Z"
+                fill="white" />
+            </svg>
+            <template #content>
+              <div style="width: 300px;">
+                <slot name="tooltip-content"></slot>
+              </div>
+            </template>
+          </el-tooltip> 
+        </slot>
+      </div>
+      <div class="aside">
+        <slot name="aside"></slot>
+      </div>
+    </div>
+    <slot name="content"></slot>
+  </el-card>
+</template>
+<script lang="ts" setup>
+import { computed } from 'vue'
+
+const props = defineProps({
+  title: {
+    type: String,
+    default: ''
+  },
+  tooltip: {
+    type: String,
+    default: ''
+  },
+  style: {
+    type: Object,
+    default: () => ({})
+  },
+  cardStyle: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+const computedCardStyle = computed(() => {
+  return Object.keys(props.cardStyle).length > 0 ? props.cardStyle : { padding: '10px 14px' }
+})
+</script>
+<style lang="scss" scoped>
+
+.top-info {
+  color: rgba(18, 18, 18, 1);
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 25px;
+  font-family: 'Source Han Sans SC';
+
+  .title {
+    font-size: 16px;
+    font-weight: 500;
+    line-height: 20px;
+    padding: 4px 0;
+    vertical-align: baseline;
+  }
+
+  .aside {
+    display: flex;
+  }
+}
+
+</style>

+ 42 - 41
src/views/count/device/components/selectForm.vue

@@ -1,41 +1,45 @@
 <template>
-  <el-card class="select-form-card" shadow="none" style="padding: 10px 14px 0;">
-    <div class="card-title no-padding">{{ title }}</div>
-    <div class="select-form">
-      <el-form :inline="true" :model="form" label-width="0">
-        <el-form-item label="">
-          <div class="card-tabs">
-            <div class="card-tab" v-for="item in tabList" :key="item.value" 
-              :class="{ active: form.tab === item.value }" @click="handleTabChange(item.value)"
-            >
-              {{ item.label }}
-            </div>
-          </div>
-        </el-form-item>
-        <el-form-item label="">
-          <el-select v-model="form.version" placeholder="">
-            <el-option label="全部版本" value="all" />
-            <el-option label="1.0.0" value="1" />
-            <el-option label="1.0.1" value="2" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="">
-          <el-select v-model="form.channel" placeholder="">
-            <el-option label="全部渠道" value="all" />
-            <el-option label="应用宝" value="1" />
-            <el-option label="华为应用市场" value="2" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="">
-          <el-date-picker :type="'daterange'" :clearable="false" style="width: 242px;" v-model="form.dateArray"
-            range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" />
-        </el-form-item>
-      </el-form>
-    </div>
-  </el-card>
+  <div class="select-form-card">
+    <LayoutHeader :title="title" :style="{ marginBottom: '23px' }" :cardStyle="{ padding: '10px 14px 0px' }">
+      <template #content>
+        <div class="select-form">
+          <el-form :inline="true" :model="form" label-width="0">
+            <el-form-item label="">
+              <div class="card-tabs">
+                <div class="card-tab" v-for="item in tabList" :key="item.value" 
+                  :class="{ active: form.tab === item.value }" @click="handleTabChange(item.value)"
+                >
+                  {{ item.label }}
+                </div>
+              </div>
+            </el-form-item>
+            <el-form-item label="">
+              <el-select v-model="form.version" placeholder="">
+                <el-option label="全部版本" value="all" />
+                <el-option label="1.0.0" value="1" />
+                <el-option label="1.0.1" value="2" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="">
+              <el-select v-model="form.channel" placeholder="">
+                <el-option label="全部渠道" value="all" />
+                <el-option label="应用宝" value="1" />
+                <el-option label="华为应用市场" value="2" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="">
+              <el-date-picker :type="'daterange'" :clearable="false" style="width: 242px;" v-model="form.dateArray"
+                range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" :disabled-date="disabledDate" />
+            </el-form-item>
+          </el-form>
+        </div>
+      </template>
+    </LayoutHeader>
+  </div>
 </template>
 <script lang="ts" name="selectForm" setup>
 import { ref, PropType } from 'vue'
+import LayoutHeader from '/@/views/count/components/LayoutHeader.vue';
 
 const props = defineProps({
   tabList: {
@@ -57,17 +61,14 @@ const props = defineProps({
   }
 })
 
-// const form = ref({
-//   tab: 1,
-//   version: 'all',
-//   channel: 'all',
-//   dateArray: [new Date(new Date().setDate(new Date().getDate() - 7)), new Date()]
-// })
-
 const handleTabChange = (tab: number) => {
   props.form!.tab = tab;
 }
 
+const disabledDate = (time: Date) => {
+  return time.getTime() > Date.now(); 
+}
+
 </script>
 
 <style scoped lang="scss">

+ 0 - 32
src/views/count/device/device/index.vue

@@ -2,38 +2,6 @@
   <div class="layout-padding device">
     <el-row :gutter="12" style="padding:0 12px 0; row-gap: 12px;">
       <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
-        <!-- <el-card class="select-form-card" shadow="none" style="padding: 10px 14px 0;">
-          <div class="card-title no-padding">设备终端</div>
-          <div class="select-form">
-            <el-form :inline="true" :model="form" label-width="0">
-              <el-form-item label="">
-                <div class="card-tabs">
-                  <div class="card-tab" :class="{ active: form.tab === 1 }" @click="handleTabChange(1)">机型</div>
-                  <div class="card-tab" :class="{ active: form.tab === 2 }" @click="handleTabChange(2)">分辨率</div>
-                  <div class="card-tab" :class="{ active: form.tab === 3 }" @click="handleTabChange(3)">操作系统</div>
-                </div>
-              </el-form-item>
-              <el-form-item label="">
-                <el-select v-model="form.version" placeholder="">
-                  <el-option label="全部版本" value="all" />
-                  <el-option label="1.0.0" value="1" />
-                  <el-option label="1.0.1" value="2" />
-                </el-select>
-              </el-form-item>
-              <el-form-item label="">
-                <el-select v-model="form.channel" placeholder="">
-                  <el-option label="全部渠道" value="all" />
-                  <el-option label="应用宝" value="1" />
-                  <el-option label="华为应用市场" value="2" />
-                </el-select>
-              </el-form-item>
-              <el-form-item label="">
-                <el-date-picker :type="'daterange'" :clearable="false" style="width: 242px;" v-model="form.dateArray" range-separator="至"
-                  start-placeholder="开始日期" end-placeholder="结束日期" />
-              </el-form-item>
-            </el-form>
-          </div>
-        </el-card> -->
         <SelectForm :tabList="tabList" :title="'设备终端'" :form="form" />
       </el-col>
     </el-row>

+ 7 - 8
src/views/count/device/location/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="layout-padding device">
+  <div class="layout-padding location">
     <el-row :gutter="12" style="padding:0 12px 0; row-gap: 12px;">
       <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
         <SelectForm :tabList="tabList" :title="'地域'" :form="form" />
@@ -9,7 +9,7 @@
     <el-row :gutter="12" style="padding: 12px 12px 12px; row-gap: 12px;">
       <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
         <el-card shadow="none">
-          <div class="device-container">
+          <div class="location-container">
             <div class="title">TOP10省市
               <el-tooltip effect="light" content="" placement="right-start">
                 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -63,8 +63,7 @@
 
           <div class="chart-container">
             <ExportToCSV :size="10" :tableStyle="{ width: '100%', maxWidth: '1476px', margin: '0 auto' }"
-              :hideTable="false" :data="formatData" :columns="columns" :fileName="title + '- 地域'"
-              @province-click="handleProvinceClick" />
+              :hideTable="false" :data="formatData" :columns="columns" :fileName="title + '- 地域'" />
           </div>
         </el-card>
       </el-col>
@@ -77,7 +76,7 @@
   </div>
 </template>
 
-<script lang="ts" name="device" setup>
+<script lang="ts" name="location" setup>
 import { ref, computed } from 'vue'
 import HorizontalBarChart from '/@/views/count/components/HorizontalBarChart.vue'
 import { formatDate } from '/@/utils/formatTime'
@@ -154,7 +153,7 @@ const title = computed(() => {
 
 // 定义表格列配置
 const columns = [
-  { prop: 'province', label: '省市' },
+  { prop: 'province', label: '省市', type: 'link', handleClick: handleProvinceClick },
   { prop: 'newUser', label: '新增用户' },
   { prop: 'newUserPercentage', label: '新增用户占比' },
   { prop: 'activeUser', label: '活跃用户' },
@@ -183,8 +182,8 @@ const formatData = computed(() => {
 <style scoped lang="scss">
 @import '/@/views/count/styles/common.scss';
 
-.device {
-  .device-container {
+.location {
+  .location-container {
     padding: 10px 14px;
     position: relative;
 

+ 34 - 26
src/views/count/engagement/components/SelectForm.vue

@@ -1,32 +1,35 @@
 <template>
-  <el-card class="select-form-card" shadow="none" style="padding: 10px 14px 0px 14px;">
-    <div class="card-title no-padding">{{ title }}</div>
-    <div class="select-form">
-      <el-form :inline="true" :model="form" label-width="0">
-        <el-form-item label="">
-          <el-date-picker v-if="type === 'date'" :type="type" style="width: 147px; min-width: 147px;" v-model="form.date" placeholder="开始日期" />
-          <el-date-picker v-if="type === 'daterange'" :type="type" style="width: 242px;" v-model="form.dateArray" range-separator="至"
-            start-placeholder="开始日期" end-placeholder="结束日期" />
-        </el-form-item>
-        <el-form-item label="">
-          <el-select v-model="form.channel" placeholder="请选择渠道">
-            <el-option label="全部渠道" value="all" />
-            <el-option label="渠道1" value="1" />
-            <el-option label="渠道2" value="2" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="">
-          <el-select v-model="form.version" placeholder="请选择版本">
-            <el-option label="全部版本" value="all" />
-            <el-option label="版本1" value="1" />
-            <el-option label="版本2" value="2" />
-          </el-select>
-        </el-form-item>
-      </el-form>
-    </div>
-  </el-card>
+  <LayoutHeader :title="title" :style="{ marginBottom: '0' }" :cardStyle="{ padding: '10px 14px 0px 14px' }">
+    <template #content>
+      <div class="select-form">
+        <el-form :inline="true" :model="form" label-width="0">
+          <el-form-item label="">
+            <el-date-picker v-if="type === 'date'" :type="type" style="width: 147px; min-width: 147px;" v-model="form.date" placeholder="开始日期" :disabled-date="disabledDate" />
+            <el-date-picker v-if="type === 'daterange'" :type="type" style="width: 242px;" v-model="form.dateArray" range-separator="至"
+              start-placeholder="开始日期" end-placeholder="结束日期" :disabled-date="disabledDate" />
+          </el-form-item>
+          <el-form-item label="">
+            <el-select v-model="form.channel" placeholder="请选择渠道">
+              <el-option label="全部渠道" value="all" />
+              <el-option label="渠道1" value="1" />
+              <el-option label="渠道2" value="2" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="">
+            <el-select v-model="form.version" placeholder="请选择版本">
+              <el-option label="全部版本" value="all" />
+              <el-option label="版本1" value="1" />
+              <el-option label="版本2" value="2" />
+            </el-select>
+          </el-form-item>
+        </el-form>
+      </div>
+    </template>
+  </LayoutHeader>
 </template>
 <script setup lang="ts">
+import LayoutHeader from '/@/views/count/components/LayoutHeader.vue';
+
 const props = defineProps({
   type: {
     type: String,
@@ -41,6 +44,11 @@ const props = defineProps({
     default: ''
   }
 })
+
+const disabledDate = (time: Date) => {
+  return time.getTime() > Date.now(); 
+}
+
 </script>
 <style scoped lang="scss">
 @import '/@/views/count/styles/common.scss';