index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. <template>
  2. <div class="layout-padding">
  3. <div class="overview">
  4. <el-row :gutter="12" style="padding: 0 12px 12px; row-gap: 12px;">
  5. <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
  6. <el-card shadow="none" style="padding: 10px 14px;">
  7. <div class="top-info">
  8. <div class="title">流失概况</div>
  9. <div class="aside">
  10. <div class="data-source-status">数据源状态: Demo数据</div>
  11. <el-button class="goto-smart-operation" type="primary" link>前往智能运营发短信
  12. <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
  13. <path d="M10.6667 4.66699L13.3334 7.33366L10.6667 10.0003" stroke="#167AF0" stroke-width="1.5"
  14. stroke-linecap="round" stroke-linejoin="round" />
  15. <path d="M2.66671 12.6663V8.33301C2.66671 7.78071 3.11441 7.33301 3.66671 7.33301H13.3334"
  16. stroke="#167AF0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
  17. </svg>
  18. </el-button>
  19. </div>
  20. </div>
  21. <div class="top-content">
  22. <div class="el-card" style="flex: 1; overflow: visible;">
  23. <div class="content-item">
  24. <div class="content-item-left">
  25. <ProgressRing
  26. :progress="0.7223"
  27. :size="111"
  28. background-color="rgba(239, 239, 239, 1)"
  29. progress-color="rgba(0, 188, 113, 1)"
  30. :stroke-width="15"
  31. :soft-corner="true"
  32. />
  33. </div>
  34. <div class="content-item-right">
  35. <div class="content-item-title">当周卸载流失设备数</div>
  36. <div class="content-item-value">38</div>
  37. <div class="content-item-percent">环比<span>-74.23%</span>
  38. <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
  39. <mask id="mask0_611_555" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="14"
  40. height="14">
  41. <rect width="14" height="14" fill="#D9D9D9" />
  42. </mask>
  43. <g mask="url(#mask0_611_555)">
  44. <path
  45. 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"
  46. fill="#00BC71" />
  47. </g>
  48. </svg>
  49. </div>
  50. </div>
  51. </div>
  52. </div>
  53. <div class="el-card" style="flex: 1; overflow: visible;">
  54. <div class="content-item">
  55. <div class="content-item-left">
  56. <ProgressRing
  57. :progress="0.38"
  58. :size="111"
  59. background-color="rgba(239, 239, 239, 1)"
  60. progress-color="rgba(230, 66, 66, 1)"
  61. :stroke-width="15"
  62. :soft-corner="true"
  63. />
  64. </div>
  65. <div class="content-item-right">
  66. <div class="content-item-title">当周卸载召回设备数</div>
  67. <div class="content-item-value">38</div>
  68. <div class="content-item-percent">
  69. 环比<span>+74.23%</span>
  70. <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
  71. <mask id="mask0_611_558" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="14"
  72. height="14">
  73. <rect width="14" height="14" transform="matrix(1 0 0 -1 0 14)" fill="#D9D9D9" />
  74. </mask>
  75. <g mask="url(#mask0_611_558)">
  76. <path
  77. 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"
  78. fill="#E64242" />
  79. </g>
  80. </svg>
  81. </div>
  82. </div>
  83. </div>
  84. </div>
  85. </div>
  86. </el-card>
  87. </el-col>
  88. </el-row>
  89. <el-row :gutter="12" style="padding: 0 12px 12px; row-gap: 12px;">
  90. <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
  91. <el-card shadow="none">
  92. <div class="trend-container">
  93. <div class="title">流失趋势</div>
  94. <div class="tabs">
  95. <div class="tabs-item" :class="{ active: activeTab === 'churnTrend' }" @click="handleTabClick('churnTrend')">卸载流失设备</div>
  96. <div class="tabs-item" :class="{ active: activeTab === 'recallTrend' }" @click="handleTabClick('recallTrend')">卸载召回设备</div>
  97. </div>
  98. <!-- 折线图 -->
  99. <div class="chart-container">
  100. <LineChart :data="currentChartData" :color="'#167af0'" :title="currentChartTitle" height="270px"
  101. :smooth="false" :area-style="true" />
  102. <div class="echarts-name">{{currentChartTitle}}</div>
  103. </div>
  104. <el-divider style="margin: 30px 0; background: rgba(230, 230, 230, 1);"/>
  105. <div class="table-container">
  106. <div class="btn-toggle-table" :class="{ 'hide-table': hideTable }" @click="hideTable = !hideTable">
  107. {{ hideTable ? '展开' : '收起' }}明细数据<svg
  108. width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
  109. <path d="M10.5 8.75L7 5.25L3.5 8.75" stroke="#167AF0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
  110. </svg>
  111. </div>
  112. <el-table class="statistics-table" :data="state.dataList" row-key="date" style="width: 100%"
  113. border :cell-style="tableStyle.cellStyle"
  114. :header-cell-style="tableStyle.headerCellStyle"
  115. v-if="!hideTable"
  116. >
  117. <el-table-column :label="'日期'" prop="date" show-overflow-tooltip></el-table-column>
  118. <el-table-column :label="'卸载流失设备'" :formatter="statusFormatter" prop="churn" show-overflow-tooltip></el-table-column>
  119. <el-table-column :label="'卸载召回设备'" :formatter="statusFormatter" prop="recall" show-overflow-tooltip></el-table-column>
  120. </el-table>
  121. <pagination
  122. v-if="!hideTable"
  123. @current-change="currentChangeHandle"
  124. @size-change="sizeChangeHandle"
  125. v-bind="state.pagination">
  126. </pagination>
  127. </div>
  128. </div>
  129. </el-card>
  130. </el-col>
  131. </el-row>
  132. </div>
  133. </div>
  134. </template>
  135. <script lang="ts" name="churnOverview" setup>
  136. import LineChart from './echarts/LineChart.vue'
  137. import ProgressRing from '../echarts/ProgressRing.vue'
  138. import { BasicTableProps, useTable } from '/@/hooks/table';
  139. import { ref, computed, reactive } from 'vue'
  140. // 流失数据
  141. const churnData = ref([
  142. { name: '流失设备', value: 38, itemStyle: { color: 'rgba(0, 188, 113, 1)' } },
  143. { name: '其他', value: 62, itemStyle: { color: 'rgba(239, 239, 239, 1)' } }
  144. ])
  145. // 召回数据
  146. const recallData = ref([
  147. { name: '召回设备', value: 38, itemStyle: { color: 'rgba(230, 66, 66, 1)' } },
  148. { name: '其他', value: 62, itemStyle: { color: 'rgba(239, 239, 239, 1)' } }
  149. ])
  150. // 流失趋势数据
  151. const churnTrendData = ref([
  152. { date: '2025-01-01', value: 25 },
  153. { date: '2025-01-02', value: 32 },
  154. { date: '2025-01-03', value: 28 },
  155. { date: '2025-01-04', value: 45 },
  156. { date: '2025-01-05', value: 38 },
  157. { date: '2025-01-06', value: 42 },
  158. { date: '2025-01-07', value: 35 },
  159. { date: '2025-01-08', value: 48 },
  160. { date: '2025-01-09', value: 52 },
  161. { date: '2025-01-10', value: 39 },
  162. { date: '2025-01-11', value: 44 },
  163. { date: '2025-01-12', value: 37 },
  164. { date: '2025-01-13', value: 41 },
  165. { date: '2025-01-14', value: 46 }
  166. ])
  167. // 召回趋势数据
  168. const recallTrendData = ref([
  169. { date: '2025-01-01', value: 15 },
  170. { date: '2025-01-02', value: 22 },
  171. { date: '2025-01-03', value: 18 },
  172. { date: '2025-01-04', value: 35 },
  173. { date: '2025-01-05', value: 28 },
  174. { date: '2025-01-06', value: 32 },
  175. { date: '2025-01-07', value: 25 },
  176. { date: '2025-01-08', value: 38 },
  177. { date: '2025-01-09', value: 42 },
  178. { date: '2025-01-10', value: 29 },
  179. { date: '2025-01-11', value: 34 },
  180. { date: '2025-01-12', value: 27 },
  181. { date: '2025-01-13', value: 31 },
  182. { date: '2025-01-14', value: 36 }
  183. ])
  184. const activeTab = ref('churnTrend')
  185. // 计算当前图表数据
  186. const currentChartData = computed(() => {
  187. return activeTab.value === 'churnTrend' ? churnTrendData.value : recallTrendData.value
  188. })
  189. // 计算当前图表标题
  190. const currentChartTitle = computed(() => {
  191. return activeTab.value === 'churnTrend' ? '卸载流失设备' : '卸载召回设备'
  192. })
  193. const handleTabClick = (tab: string) => {
  194. activeTab.value = tab
  195. }
  196. // 表格数据
  197. const hideTable = ref(false);
  198. const state: BasicTableProps = reactive<BasicTableProps>({
  199. queryForm: {
  200. ip: '',
  201. },
  202. pageList: () => Promise.resolve([]),
  203. pagination: {
  204. current: 1,
  205. size: 10,
  206. total: 0,
  207. pageSizes: [5, 10, 20, 50, 100]
  208. },
  209. dataList: [
  210. {
  211. date: '2025-01-01',
  212. churn: 10,
  213. recall: 20
  214. },
  215. {
  216. date: '2025-01-02',
  217. churn: 15,
  218. recall: 25
  219. },
  220. {
  221. date: '2025-01-03',
  222. churn: 20,
  223. recall: 30
  224. },
  225. {
  226. date: '2025-01-04',
  227. churn: 25,
  228. recall: 35
  229. }
  230. ]
  231. });
  232. const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
  233. const statusFormatter = (row: any, column: any, cellValue: any, index: any) => {
  234. return cellValue || '--';
  235. }
  236. </script>
  237. <style scoped lang="scss">
  238. svg {
  239. vertical-align: middle;
  240. margin: 0 0 0 12px;
  241. }
  242. .overview {
  243. font-family: Source Han Sans SC;
  244. color: rgba(18, 18, 18, 1);
  245. }
  246. .top-info {
  247. color: rgba(18, 18, 18, 1);
  248. display: flex;
  249. justify-content: space-between;
  250. align-items: center;
  251. margin-bottom: 25px;
  252. .title {
  253. font-size: 16px;
  254. font-weight: 500;
  255. line-height: 20px;
  256. padding: 4px 0;
  257. }
  258. .aside {
  259. display: flex;
  260. }
  261. .data-source-status {
  262. font-weight: 400;
  263. font-size: 14px;
  264. line-height: 20px;
  265. color: rgba(18, 18, 18, 1);
  266. padding: 4px 16px 4px 0;
  267. }
  268. .goto-smart-operation {
  269. padding: 4px 12px;
  270. border-radius: 4px;
  271. border: 1px solid rgba(22, 122, 240, 1);
  272. font-weight: 400;
  273. font-size: 14px;
  274. line-height: 20px;
  275. color: rgba(22, 122, 240, 1);
  276. }
  277. }
  278. .top-content {
  279. display: flex;
  280. gap: 20px;
  281. .content-item {
  282. display: flex;
  283. align-items: center;
  284. gap: 0;
  285. flex: 1;
  286. padding: 27.5px;
  287. justify-content: center;
  288. }
  289. .content-item-left {
  290. display: flex;
  291. align-items: center;
  292. justify-content: center;
  293. margin-right: 30px;
  294. }
  295. .content-item-right {
  296. // flex: 1;
  297. }
  298. .content-item-title {
  299. color: rgba(18, 18, 18, 1);
  300. font-weight: 400;
  301. font-size: 15px;
  302. line-height: 20px;
  303. margin-bottom: 12px;
  304. }
  305. .content-item-value {
  306. font-weight: 500;
  307. font-style: Medium;
  308. font-size: 28px;
  309. margin-bottom: 16px;
  310. line-height: 41px;
  311. }
  312. .content-item-percent {
  313. font-weight: 400;
  314. font-size: 14px;
  315. vertical-align: middle;
  316. line-height: 20px;
  317. }
  318. .content-item-percent span {
  319. color: rgba(0, 188, 113, 1);
  320. }
  321. }
  322. .trend-container {
  323. padding: 10px 14px;
  324. .title {
  325. line-height: 19px;
  326. font-weight: 500;
  327. font-size: 16px;
  328. padding-left: 12px;
  329. position: relative;
  330. margin-bottom: 53px;
  331. &::before {
  332. content: '';
  333. position: absolute;
  334. left: 0;
  335. top: 50%;
  336. transform: translateY(-50%);
  337. width: 4px;
  338. height: 14px;
  339. background: rgba(22, 122, 240, 1);
  340. }
  341. }
  342. .tabs {
  343. display: flex;
  344. margin-left: 85px;
  345. .tabs-item {
  346. padding: 0 12px;
  347. height: 32px;
  348. line-height: 30px;
  349. border: 1px solid rgba(22, 122, 240, 1);
  350. text-align: center;
  351. color: rgba(22, 122, 240, 1);
  352. cursor: pointer;
  353. &.active {
  354. background: rgba(22, 122, 240, 1);
  355. color: #ffffff;
  356. }
  357. }
  358. }
  359. .chart-container {
  360. margin: 20px 85px 0;
  361. text-align: center;
  362. }
  363. .echarts-name {
  364. display: inline-block;
  365. margin: 28px auto 0;
  366. padding-left: 16px;
  367. font-weight: 400;
  368. font-size: 14px;
  369. line-height: 20px;
  370. color: rgba(18, 18, 18, 1);
  371. position: relative;
  372. &::before {
  373. content: '';
  374. position: absolute;
  375. left: 0;
  376. top: 50%;
  377. transform: translateY(-50%);
  378. width: 8px;
  379. height: 8px;
  380. background: rgba(22, 122, 240, 1);
  381. border-radius: 50%;
  382. }
  383. }
  384. .table-container {
  385. padding: 0 85px;
  386. .btn-toggle-table {
  387. font-weight: 500;
  388. font-size: 14px;
  389. line-height: 20px;
  390. color: rgba(22, 122, 240, 1);
  391. margin-bottom: 20px;
  392. cursor: pointer;
  393. svg {
  394. transition: transform 0.3s ease-in-out;
  395. }
  396. &.hide-table svg {
  397. transform: rotate(-180deg);
  398. }
  399. }
  400. .table-container {
  401. margin-top: 20px;
  402. }
  403. }
  404. }
  405. </style>