|
@@ -0,0 +1,428 @@
|
|
|
+<template>
|
|
|
+ <div class="overview">
|
|
|
+ <el-row :gutter="12" style="padding: 12px; row-gap: 12px;">
|
|
|
+ <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
|
|
|
+ <el-card shadow="hover" 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>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="top-content">
|
|
|
+ <div class="el-card" style="flex: 1; overflow: visible;">
|
|
|
+ <div class="content-item">
|
|
|
+ <div class="content-item-left">
|
|
|
+ <DoughnutChart :data="churnData" width="150px" height="150px" :radius="['35px', '55px']"
|
|
|
+ :show-label="false" :show-legend="false" />
|
|
|
+ </div>
|
|
|
+ <div class="content-item-right">
|
|
|
+ <div class="content-item-title">当周卸载流失设备数</div>
|
|
|
+ <div class="content-item-value">38</div>
|
|
|
+ <div class="content-item-percent">环比<span>-74.23%</span>
|
|
|
+ <svg 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" />
|
|
|
+ </mask>
|
|
|
+ <g mask="url(#mask0_611_555)">
|
|
|
+ <path
|
|
|
+ d="M11.1702 4.43275C11.5204 4.43302 11.8046 4.71698 11.8047 5.06725L11.8041 10.7783C11.8041 11.1287 11.5201 11.4128 11.1696 11.4128L5.45857 11.4134C5.10831 11.4133 4.82435 11.1291 4.82408 10.7789C4.82408 10.4284 5.1087 10.1438 5.45916 10.1438L9.6377 10.1438L2.69566 3.20174C2.44785 2.95393 2.44785 2.55215 2.69566 2.30434C2.94348 2.05652 3.34526 2.05652 3.59307 2.30434L10.5351 9.24637L10.5351 5.06783C10.5351 4.71737 10.8197 4.43275 11.1702 4.43275Z"
|
|
|
+ fill="#00BC71" />
|
|
|
+ </g>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="el-card" style="flex: 1; overflow: visible;">
|
|
|
+ <div class="content-item">
|
|
|
+ <div class="content-item-left">
|
|
|
+ <DoughnutChart :data="recallData" width="150px" height="150px" :radius="['35px', '55px']"
|
|
|
+ :show-label="false" :show-legend="false" />
|
|
|
+ </div>
|
|
|
+ <div class="content-item-right">
|
|
|
+ <div class="content-item-title">当周卸载召回设备数</div>
|
|
|
+ <div class="content-item-value">38</div>
|
|
|
+ <div class="content-item-percent">
|
|
|
+ 环比<span>+74.23%</span>
|
|
|
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <mask id="mask0_611_558" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="14"
|
|
|
+ height="14">
|
|
|
+ <rect width="14" height="14" transform="matrix(1 0 0 -1 0 14)" fill="#D9D9D9" />
|
|
|
+ </mask>
|
|
|
+ <g mask="url(#mask0_611_558)">
|
|
|
+ <path
|
|
|
+ d="M11.1702 9.56725C11.5204 9.56698 11.8046 9.28302 11.8047 8.93275L11.8041 3.22173C11.8041 2.87127 11.5201 2.58723 11.1696 2.58723L5.45857 2.58665C5.10831 2.58668 4.82435 2.87093 4.82408 3.22114C4.82408 3.5716 5.1087 3.85622 5.45916 3.85622L9.6377 3.85622L2.69566 10.7983C2.44785 11.0461 2.44785 11.4479 2.69566 11.6957C2.94348 11.9435 3.34526 11.9435 3.59307 11.6957L10.5351 4.75363L10.5351 8.93217C10.5351 9.28263 10.8197 9.56725 11.1702 9.56725Z"
|
|
|
+ fill="#E64242" />
|
|
|
+ </g>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </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="hover">
|
|
|
+ <div class="trend-container">
|
|
|
+ <div class="title">流失趋势</div>
|
|
|
+ <div class="tabs">
|
|
|
+ <div class="tabs-item" :class="{ active: activeTab === 'churnTrend' }" @click="handleTabClick('churnTrend')">卸载流失设备</div>
|
|
|
+ <div class="tabs-item" :class="{ active: activeTab === 'recallTrend' }" @click="handleTabClick('recallTrend')">卸载召回设备</div>
|
|
|
+ </div>
|
|
|
+ <!-- 折线图 -->
|
|
|
+ <div class="chart-container">
|
|
|
+ <LineChart :data="currentChartData" :color="'#167af0'" :title="currentChartTitle" height="270px"
|
|
|
+ :smooth="false" :area-style="true" />
|
|
|
+ <div class="echarts-name">{{currentChartTitle}}</div>
|
|
|
+ </div>
|
|
|
+ <el-divider style="margin: 30px 0; background: rgba(230, 230, 230, 1);"/>
|
|
|
+ <div class="table-container">
|
|
|
+ <div class="btn-toggle-table" :class="{ 'hide-table': hideTable }" @click="hideTable = !hideTable">
|
|
|
+ {{ hideTable ? '展开' : '收起' }}明细数据<svg
|
|
|
+ width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <path d="M10.5 8.75L7 5.25L3.5 8.75" stroke="#167AF0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-table class="statistics-table" :data="state.dataList" row-key="date" style="width: 100%"
|
|
|
+ border :cell-style="tableStyle.cellStyle"
|
|
|
+ :header-cell-style="tableStyle.headerCellStyle"
|
|
|
+ v-if="!hideTable"
|
|
|
+ >
|
|
|
+ <el-table-column :label="'日期'" prop="date" show-overflow-tooltip></el-table-column>
|
|
|
+ <el-table-column :label="'卸载流失设备'" :formatter="statusFormatter" prop="churn" show-overflow-tooltip></el-table-column>
|
|
|
+ <el-table-column :label="'卸载召回设备'" :formatter="statusFormatter" prop="recall" show-overflow-tooltip></el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <pagination
|
|
|
+ v-if="!hideTable"
|
|
|
+ @current-change="currentChangeHandle"
|
|
|
+ @size-change="sizeChangeHandle"
|
|
|
+ v-bind="state.pagination">
|
|
|
+ </pagination>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts" name="churnOverview" setup>
|
|
|
+import DoughnutChart from './echarts/DoughnutChart.vue'
|
|
|
+import LineChart from './echarts/LineChart.vue'
|
|
|
+import { BasicTableProps, useTable } from '/@/hooks/table';
|
|
|
+import { ref, computed, reactive } from 'vue'
|
|
|
+
|
|
|
+// 流失数据
|
|
|
+const churnData = ref([
|
|
|
+ { name: '流失设备', value: 38, itemStyle: { color: 'rgba(0, 188, 113, 1)' } },
|
|
|
+ { name: '其他', value: 62, itemStyle: { color: 'rgba(239, 239, 239, 1)' } }
|
|
|
+])
|
|
|
+
|
|
|
+// 召回数据
|
|
|
+const recallData = ref([
|
|
|
+ { name: '召回设备', value: 38, itemStyle: { color: 'rgba(230, 66, 66, 1)' } },
|
|
|
+ { name: '其他', value: 62, itemStyle: { color: 'rgba(239, 239, 239, 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 currentChartTitle = computed(() => {
|
|
|
+ return activeTab.value === 'churnTrend' ? '卸载流失设备' : '卸载召回设备'
|
|
|
+})
|
|
|
+
|
|
|
+const handleTabClick = (tab: string) => {
|
|
|
+ activeTab.value = tab
|
|
|
+}
|
|
|
+
|
|
|
+// 表格数据
|
|
|
+const hideTable = ref(false);
|
|
|
+const state: BasicTableProps = reactive<BasicTableProps>({
|
|
|
+ queryForm: {
|
|
|
+ ip: '',
|
|
|
+ },
|
|
|
+ pageList: () => Promise.resolve([]),
|
|
|
+ pagination: {
|
|
|
+ current: 1,
|
|
|
+ size: 10,
|
|
|
+ total: 0,
|
|
|
+ pageSizes: [5, 10, 20, 50, 100]
|
|
|
+ },
|
|
|
+ dataList: [
|
|
|
+ {
|
|
|
+ date: '2025-01-01',
|
|
|
+ churn: 10,
|
|
|
+ recall: 20
|
|
|
+ },
|
|
|
+ {
|
|
|
+ date: '2025-01-02',
|
|
|
+ churn: 15,
|
|
|
+ recall: 25
|
|
|
+ },
|
|
|
+ {
|
|
|
+ date: '2025-01-03',
|
|
|
+ churn: 20,
|
|
|
+ recall: 30
|
|
|
+ },
|
|
|
+ {
|
|
|
+ date: '2025-01-04',
|
|
|
+ churn: 25,
|
|
|
+ recall: 35
|
|
|
+ }
|
|
|
+ ]
|
|
|
+});
|
|
|
+const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
|
|
|
+const statusFormatter = (row: any, column: any, cellValue: any, index: any) => {
|
|
|
+ return cellValue || '--';
|
|
|
+}
|
|
|
+
|
|
|
+</script>
|
|
|
+<style scoped lang="scss">
|
|
|
+svg {
|
|
|
+ vertical-align: middle;
|
|
|
+ margin: 0 0 0 12px;
|
|
|
+}
|
|
|
+.overview {
|
|
|
+ font-family: Source Han Sans SC;
|
|
|
+ color: rgba(18, 18, 18, 1);
|
|
|
+}
|
|
|
+
|
|
|
+.top-info {
|
|
|
+ color: rgba(18, 18, 18, 1);
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 25px;
|
|
|
+
|
|
|
+
|
|
|
+ .title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 500;
|
|
|
+ line-height: 20px;
|
|
|
+ padding: 4px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .aside {
|
|
|
+ display: flex;
|
|
|
+ }
|
|
|
+
|
|
|
+ .data-source-status {
|
|
|
+ font-weight: 400;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 20px;
|
|
|
+ color: rgba(18, 18, 18, 1);
|
|
|
+ padding: 4px 16px 4px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .goto-smart-operation {
|
|
|
+ padding: 4px 12px;
|
|
|
+ border-radius: 4px;
|
|
|
+ border: 1px solid rgba(22, 122, 240, 1);
|
|
|
+ font-weight: 400;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 20px;
|
|
|
+ color: rgba(22, 122, 240, 1);
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+.top-content {
|
|
|
+ display: flex;
|
|
|
+ gap: 20px;
|
|
|
+
|
|
|
+ .content-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 0;
|
|
|
+ flex: 1;
|
|
|
+ padding: 27.5px;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .content-item-left {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .content-item-right {
|
|
|
+ // flex: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .content-item-title {
|
|
|
+ color: rgba(18, 18, 18, 1);
|
|
|
+ font-weight: 400;
|
|
|
+ font-size: 15px;
|
|
|
+ line-height: 20px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .content-item-value {
|
|
|
+ font-weight: 500;
|
|
|
+ font-style: Medium;
|
|
|
+ font-size: 28px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ line-height: 41px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .content-item-percent {
|
|
|
+ font-weight: 400;
|
|
|
+ font-size: 14px;
|
|
|
+ vertical-align: middle;
|
|
|
+ line-height: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .content-item-percent span {
|
|
|
+ color: rgba(0, 188, 113, 1);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.trend-container {
|
|
|
+ padding: 10px 14px;
|
|
|
+ .title {
|
|
|
+ line-height: 19px;
|
|
|
+ font-weight: 500;
|
|
|
+ font-size: 16px;
|
|
|
+ padding-left: 12px;
|
|
|
+ position: relative;
|
|
|
+ margin-bottom: 53px;
|
|
|
+ &::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ top: 50%;
|
|
|
+ transform: translateY(-50%);
|
|
|
+ width: 4px;
|
|
|
+ height: 14px;
|
|
|
+ background: rgba(22, 122, 240, 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .tabs {
|
|
|
+ width: 240px;
|
|
|
+ display: flex;
|
|
|
+ margin-left: 85px;
|
|
|
+ .tabs-item {
|
|
|
+ flex: 1;
|
|
|
+ height: 32px;
|
|
|
+ line-height: 30px;
|
|
|
+ border: 1px solid rgba(22, 122, 240, 1);
|
|
|
+ text-align: center;
|
|
|
+ color: rgba(22, 122, 240, 1);
|
|
|
+ cursor: pointer;
|
|
|
+ &.active {
|
|
|
+ background: rgba(22, 122, 240, 1);
|
|
|
+ color: #ffffff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-container {
|
|
|
+ margin: 20px 85px 0;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .echarts-name {
|
|
|
+ display: inline-block;
|
|
|
+ margin: 28px auto 0;
|
|
|
+ padding-left: 16px;
|
|
|
+ font-weight: 400;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 20px;
|
|
|
+ color: rgba(18, 18, 18, 1);
|
|
|
+ position: relative;
|
|
|
+ &::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ top: 50%;
|
|
|
+ transform: translateY(-50%);
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ background: rgba(22, 122, 240, 1);
|
|
|
+ border-radius: 50%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-container {
|
|
|
+ padding: 0 85px;
|
|
|
+
|
|
|
+ .btn-toggle-table {
|
|
|
+ font-weight: 500;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 20px;
|
|
|
+ color: rgba(22, 122, 240, 1);
|
|
|
+ margin-bottom: 20px;
|
|
|
+ cursor: pointer;
|
|
|
+ svg {
|
|
|
+ transition: transform 0.3s ease-in-out;
|
|
|
+ }
|
|
|
+ &.hide-table svg {
|
|
|
+ transform: rotate(-180deg);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-container {
|
|
|
+ margin-top: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+</style>
|