index.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798
  1. <template>
  2. <div class="History">
  3. <el-breadcrumb separator-class="el-icon-arrow-right">
  4. <el-breadcrumb-item>新媒体</el-breadcrumb-item>
  5. <el-breadcrumb-item>用户分析</el-breadcrumb-item>
  6. <el-breadcrumb-item>用户趋势</el-breadcrumb-item>
  7. </el-breadcrumb>
  8. <el-card class="box-card">
  9. <div class="head">
  10. <div class="head-item">
  11. <el-form
  12. ref="form"
  13. :model="form"
  14. size="small"
  15. :inline="true"
  16. label-width="120px"
  17. >
  18. <el-form-item label="应用">
  19. <el-select
  20. v-model="form.app"
  21. placeholder="请选择时段"
  22. @change="change"
  23. >
  24. <el-option
  25. v-for="item in cycle"
  26. :key="item.value"
  27. :label="item.label"
  28. :value="item.value"
  29. :disabled="item.disabled"
  30. >
  31. </el-option>
  32. </el-select>
  33. </el-form-item>
  34. </el-form>
  35. </div>
  36. <div class="head-item" v-if="!userTotalList.length" />
  37. <div class="head-item" v-if="!userTotalList.length" />
  38. <div
  39. v-for="(item, i) in userTotalList"
  40. :key="i + item.key"
  41. class="head-item"
  42. :style="{
  43. borderTopColor: type === item.key ? '#396fff' : '#fff',
  44. }"
  45. >
  46. <div>{{ item.name }}</div>
  47. <div class="value">
  48. <countTo
  49. v-if="!item.isNum"
  50. :startVal="0"
  51. :endVal="item.value - 0"
  52. :duration="1500"
  53. ></countTo>
  54. <span v-else v-text="item.value"></span>
  55. </div>
  56. </div>
  57. </div>
  58. </el-card>
  59. <br />
  60. <el-card class="box-card">
  61. <el-form
  62. ref="form"
  63. :model="form"
  64. size="small"
  65. :inline="true"
  66. label-width="120px"
  67. >
  68. <el-form-item label="日期">
  69. <el-date-picker
  70. v-if="form.date.length"
  71. v-model="form.date"
  72. type="daterange"
  73. :disabled-date="time => disabledDate(time)"
  74. range-separator="-"
  75. start-placeholder="开始日期"
  76. end-placeholder="结束日期"
  77. :clearable="false"
  78. >
  79. </el-date-picker>
  80. </el-form-item>
  81. <el-form-item label="版本">
  82. <el-select
  83. filterable
  84. multiple
  85. collapse-tags
  86. clearable
  87. v-model="form.version"
  88. placeholder="请选择版本"
  89. @change="changeversion"
  90. >
  91. <el-option
  92. v-for="item in version"
  93. :key="item.value"
  94. :label="item.label"
  95. :value="item.value"
  96. :disabled="item.disabled"
  97. >
  98. </el-option>
  99. </el-select>
  100. </el-form-item>
  101. <el-form-item label="渠道">
  102. <el-select
  103. filterable
  104. multiple
  105. collapse-tags
  106. clearable
  107. v-model="form.channel"
  108. @change="changechannel"
  109. placeholder="请选择渠道"
  110. >
  111. <el-option
  112. v-for="item in channel"
  113. :key="item.value"
  114. :label="item.label"
  115. :value="item.value"
  116. :disabled="item.disabled"
  117. >
  118. </el-option>
  119. </el-select>
  120. </el-form-item>
  121. <el-form-item label="来源">
  122. <el-select
  123. collapse-tags
  124. clearable
  125. v-model="form.client"
  126. placeholder="请选择来源"
  127. >
  128. <el-option
  129. v-for="item in client"
  130. :key="item.value"
  131. :label="item.label"
  132. :value="item.value"
  133. :disabled="item.disabled"
  134. >
  135. </el-option>
  136. </el-select>
  137. </el-form-item>
  138. <el-form-item style="float: right">
  139. <el-button type="primary" @click="onSubmit">查询</el-button>
  140. <el-button type="primary" @click="onExport">导出</el-button>
  141. </el-form-item>
  142. </el-form>
  143. <el-divider />
  144. <div class="head" v-if="oriData.total">
  145. <div
  146. v-for="(item, i) in oriData.total"
  147. :key="i + item.key"
  148. class="head-item"
  149. @click="() => changeData(item.key, item.name)"
  150. :style="{
  151. borderTopColor: type === item.key ? '#396fff' : '#fff',
  152. }"
  153. >
  154. <div>{{ item.name }}</div>
  155. <div class="value">
  156. <!-- oriData.total.activeUser -->
  157. <countTo
  158. v-if="!item.isNum"
  159. :startVal="0"
  160. :endVal="item.value - 0"
  161. :duration="1500"
  162. ></countTo>
  163. <span v-else v-text="item.value"></span>
  164. </div>
  165. </div>
  166. </div>
  167. <div class="realLineChart" ref="realLineChart"></div>
  168. <el-table
  169. v-if="this.oriData && this.oriData.list && this.oriData.list.length"
  170. :data="showList"
  171. style="width: 100%"
  172. :header-cell-style="{ backgroundColor: '#f4f5f7', color: '#606266' }"
  173. >
  174. <el-table-column prop="dt" label="日期" />
  175. <el-table-column prop="activeUser" sortable label="活跃用户">
  176. <template #default="scope">
  177. <countTo
  178. :startVal="scope.row.activeUser"
  179. :endVal="scope.row.activeUser"
  180. :duration="100"
  181. ></countTo>
  182. </template>
  183. </el-table-column>
  184. <el-table-column prop="newUser" sortable label="新增用户">
  185. <template #default="scope">
  186. <countTo
  187. :startVal="scope.row.newUser"
  188. :endVal="scope.row.newUser"
  189. :duration="100"
  190. ></countTo>
  191. </template>
  192. </el-table-column>
  193. <el-table-column prop="totalUser" sortable label="累计用户">
  194. <template #default="scope">
  195. <countTo
  196. :startVal="scope.row.totalUser"
  197. :endVal="scope.row.totalUser"
  198. :duration="100"
  199. ></countTo>
  200. </template>
  201. </el-table-column>
  202. <el-table-column prop="startTimes" sortable label="启动次数">
  203. <template #default="scope">
  204. <countTo
  205. :startVal="scope.row.startTimes"
  206. :endVal="scope.row.startTimes"
  207. :duration="100"
  208. ></countTo>
  209. </template>
  210. </el-table-column>
  211. <el-table-column prop="duration" sortable label="人均使用时长">
  212. <template #default="scope">
  213. {{ timeFormat(scope.row.durationUser) }}
  214. </template>
  215. </el-table-column>
  216. <el-table-column prop="duration" sortable label="次均使用时长">
  217. <template #default="scope">
  218. {{ timeFormat(scope.row.durationTimes) }}
  219. </template>
  220. </el-table-column>
  221. </el-table>
  222. <div v-if="oriData.list && oriData.list.length">
  223. <el-pagination
  224. v-if="Math.ceil(oriData.list.length / 10) > 1"
  225. :current-page="page"
  226. layout="prev, pager, next"
  227. :total="oriData.list.length"
  228. @current-change="pagechange"
  229. />
  230. </div>
  231. </el-card>
  232. </div>
  233. </template>
  234. <script>
  235. // @ is an alias to /src
  236. import {
  237. getRule,
  238. getAppList,
  239. getHistory,
  240. getSearchData,
  241. getUserTotal,
  242. } from '@/api/index';
  243. import countTo from '@/components/counto/vue-countTo.vue';
  244. import { defaultAppNameFunc } from '@/utils/tool.js';
  245. import * as echarts from 'echarts/core';
  246. import { LineChart } from 'echarts/charts';
  247. import {
  248. TitleComponent,
  249. TooltipComponent,
  250. GridComponent,
  251. ToolboxComponent,
  252. LegendComponent,
  253. } from 'echarts/components';
  254. import { CanvasRenderer } from 'echarts/renderers';
  255. echarts.use([
  256. TitleComponent,
  257. TooltipComponent,
  258. GridComponent,
  259. LineChart,
  260. CanvasRenderer,
  261. ToolboxComponent,
  262. LegendComponent,
  263. ]);
  264. // import config from "@/config/index";
  265. let chart = undefined;
  266. export default {
  267. name: 'History',
  268. data() {
  269. return {
  270. type: '',
  271. lastParams: {},
  272. page: 1,
  273. form: {
  274. app: '',
  275. version: [],
  276. channel: [],
  277. date: [],
  278. client: '',
  279. },
  280. cycle: [],
  281. oriData: {},
  282. showList: [],
  283. version: [],
  284. channel: [],
  285. client: [],
  286. userTotalList: [],
  287. };
  288. },
  289. async mounted() {
  290. if (chart && chart.dispose) chart.dispose();
  291. const { source, appV, appC, clentV, appli, appCLi, appVLi, clentli } =
  292. await this.getAppListFunc();
  293. const keys = {
  294. value: 'mname',
  295. label: 'mname',
  296. };
  297. const clentliList = clentli.find(r => r.mdefault) || false;
  298. let client = clentliList ? clentliList.mcode.toString() : -1;
  299. this.cycle = this.verifyList(appli, source, keys, false);
  300. this.channel = this.verifyList(appCLi, appC, keys, true);
  301. this.version = this.verifyList(appVLi, appV, keys, true);
  302. this.client = this.verifyList(
  303. clentli,
  304. clentV,
  305. {
  306. value: 'mcode',
  307. label: 'mname',
  308. },
  309. true
  310. );
  311. this.form = {
  312. // app: (this.cycle[0] || { value: "" }).value,
  313. app: defaultAppNameFunc(this.cycle),
  314. version: [(this.version[0] || { value: '' }).value],
  315. client,
  316. channel: [(this.channel[0] || { value: '' }).value],
  317. date: [new Date(Date.now() - 604800000), new Date(Date.now() - 86400000)],
  318. };
  319. this.onSubmit();
  320. this.getUser();
  321. },
  322. computed: {},
  323. methods: {
  324. getUser() {
  325. getUserTotal({ app: this.form.app }).then(r => {
  326. this.userTotalList = (r || []).map(v => {
  327. return {
  328. name: v.name,
  329. value: v.value,
  330. key: v.name,
  331. isNum: isNaN(v.value),
  332. };
  333. });
  334. });
  335. },
  336. verifyList(list, verify, obj, more) {
  337. if (!obj) return;
  338. let li = list || [];
  339. const out = [];
  340. more && out.push({ value: -1, label: '全部' });
  341. for (let i = 0; i < li.length; i++) {
  342. const v = li[i];
  343. if (verify.length !== 0 && !verify[v.mcode]) continue;
  344. out.push({
  345. value: v[obj.value],
  346. label: v[obj.label],
  347. });
  348. }
  349. return out;
  350. },
  351. pagechange(p) {
  352. this.page = p;
  353. this.pushShowList();
  354. },
  355. pushShowList() {
  356. let s = this.page - 1 < 0 ? 0 : (this.page - 1) * 10;
  357. let e = this.page * 10;
  358. let li = JSON.parse(JSON.stringify(this.oriData.list || []));
  359. let out = [];
  360. for (let i = s; i < e; i++) {
  361. li[i] && out.push(li[i]);
  362. }
  363. this.showList = out;
  364. },
  365. onSubmit() {
  366. this.lastParams = {
  367. app: this.form.app,
  368. start: this.FormData(this.form.date[0]),
  369. end: this.FormData(this.form.date[1]),
  370. manufacturer: this.form.channel == -1 ? undefined : this.form.channel,
  371. version: this.form.version == -1 ? undefined : this.form.version,
  372. lib: this.form.client == -1 ? undefined : this.form.client,
  373. };
  374. getHistory(this.lastParams)
  375. .then(r => {
  376. if (!this.$refs.realLineChart) return;
  377. let oriData = r || {};
  378. const total = oriData.total || {},
  379. keys = Object.keys(total),
  380. color = ['rgb(244, 127, 146)', 'rgb(17, 160, 248)'],
  381. p = [];
  382. let tab = undefined,
  383. tabName = undefined;
  384. for (let i = 0; i < keys.length; i++) {
  385. const v = keys[i];
  386. if (!total[v].value && total[v].value !== 0) continue;
  387. let isNum = isNaN(total[v].value);
  388. let value = '';
  389. if (
  390. isNum &&
  391. !/次数/g.test(total[v].name) &&
  392. !/下载量/g.test(total[v].name) &&
  393. !/装机量/g.test(total[v].name) &&
  394. !/用户数/g.test(total[v].name) &&
  395. typeof total[v].value === 'number'
  396. )
  397. value = this.timeFormat(total[v].value);
  398. else value = total[v].value;
  399. if (!tab && v !== 'downloads') {
  400. tab = v;
  401. tabName = total[v].name;
  402. }
  403. p.push({
  404. name: total[v].name,
  405. value,
  406. color: color[i % 2],
  407. key: v,
  408. isNum,
  409. });
  410. }
  411. this.oriData = {
  412. list: oriData.list || [],
  413. total: p,
  414. };
  415. this.changeData(tab, tabName);
  416. chart.hideLoading();
  417. this.page = 1;
  418. this.pushShowList();
  419. })
  420. .catch(() => {
  421. this.oriData = {};
  422. this.pushShowList();
  423. chart && chart.clear() && chart.hideLoading();
  424. });
  425. },
  426. changeData(type, title) {
  427. const ret = {
  428. downloads: true,
  429. ip: true,
  430. avgUser: true
  431. };
  432. if (ret[type]) return;
  433. this.type = type;
  434. const keyList = [],
  435. valueList = [];
  436. (this.oriData.list || []).map(v => {
  437. keyList.push(v.dt);
  438. valueList.push(v[this.type]);
  439. });
  440. chart && chart.clear();
  441. this.createImage(keyList, valueList, title);
  442. },
  443. createImage(keyList, valueList, title) {
  444. if (!chart) {
  445. chart = echarts.init(this.$refs.realLineChart);
  446. window.onresize = chart.resize;
  447. }
  448. chart.resize({
  449. height: (this.$refs.realLineChart.offsetWidth * 4) / 16,
  450. });
  451. const _this = this;
  452. chart.setOption({
  453. tooltip: {
  454. confine: true,
  455. trigger: 'axis',
  456. formatter(v) {
  457. const item = v[0] || {};
  458. let val = item.data || 0;
  459. if (/duration/.test(_this.type)) val = _this.timeFormat(val);
  460. return item.axisValue + '<br />' + title + ':' + val;
  461. },
  462. },
  463. toolbox: {
  464. feature: {
  465. saveAsImage: {
  466. type: 'jpg',
  467. name: '趋势',
  468. },
  469. },
  470. },
  471. legend: {
  472. data: [title],
  473. },
  474. grid: {
  475. left: '3%',
  476. right: '4%',
  477. bottom: '3%',
  478. containLabel: true,
  479. },
  480. xAxis: {
  481. type: 'category',
  482. boundaryGap: true,
  483. data: keyList,
  484. },
  485. yAxis: {
  486. type: 'value',
  487. scale: true,
  488. minInterval: 1,
  489. axisLabel: {
  490. formatter(v) {
  491. let val = v;
  492. if (_this.type === 'duration') val = _this.timeFormat(val);
  493. else {
  494. if (val >= 100000000) {
  495. val = (val / 100000000).toFixed(2) + '亿';
  496. } else if (val >= 10000) {
  497. val = (val / 10000).toFixed(2) + '万';
  498. }
  499. }
  500. return val;
  501. },
  502. },
  503. },
  504. series: [
  505. {
  506. name: title,
  507. data: valueList,
  508. symbolSize: 0,
  509. lineStyle: {
  510. width: 1,
  511. },
  512. type: 'line',
  513. smooth: true,
  514. color: 'rgba(58,132,255,.9)',
  515. areaStyle: {
  516. color: {
  517. type: 'linear',
  518. x: 0,
  519. y: 0,
  520. x2: 0,
  521. y2: 1,
  522. colorStops: [
  523. {
  524. offset: 0,
  525. color: 'rgba(58,132,255, 0.8)', // 0% 处的颜色
  526. },
  527. {
  528. offset: 1,
  529. color: 'rgba(58,132,255, 0.1)', // 100% 处的颜色
  530. },
  531. ],
  532. global: false, // 缺省为 false
  533. },
  534. },
  535. },
  536. ],
  537. });
  538. },
  539. async getAppListFunc() {
  540. const { r, li, appVersion, channel, clientList } =
  541. await this.getAppListOri();
  542. let source = { length: 0 },
  543. appli = [];
  544. let appV = { length: 0 },
  545. appVLi = [];
  546. let appC = { length: 0 },
  547. appCLi = [];
  548. let clentV = { length: 0 },
  549. clentli = [];
  550. let prvList =
  551. r.output && r.output.data ? r.output.data.prvRolectrl || [] : [];
  552. for (let i = 0; i < prvList.length; i++) {
  553. const v = prvList[i];
  554. if (v.controlid == 'RMT_SOURCE')
  555. (source[v.detid] = true), (source.length = source.length + 1);
  556. if (v.controlid == 'APP_VERSION')
  557. (appV[v.detid] = true), (appV.length = appV.length + 1);
  558. if (v.controlid == 'CHANNEL')
  559. (appC[v.detid] = true), (appC.length = appC.length + 1);
  560. if (v.controlid == 'CLIENT_TYPE')
  561. (clentV[v.detid] = true), (clentV.length = clentV.length + 1);
  562. }
  563. if (li.status === '0') appli = li.output.data || [];
  564. if (appVersion.length) appVLi = appVersion || [];
  565. if (channel.length) appCLi = channel || [];
  566. if (clientList.length) clentli = clientList || [];
  567. return {
  568. source,
  569. appV,
  570. appC,
  571. appli,
  572. clentV,
  573. appVLi,
  574. appCLi,
  575. clentli,
  576. };
  577. },
  578. async getAppListOri() {
  579. const roleid = JSON.parse(
  580. window.parent.localStorage.userinfo || '{}'
  581. ).roleid;
  582. const r = await getRule({
  583. db: 'authplat',
  584. exportMark: '0',
  585. menuid: 399,
  586. roleid,
  587. });
  588. // 应用列表
  589. const li = await getAppList({
  590. exportMark: '0',
  591. gcode: 'SOURCE',
  592. pageid: 1,
  593. pagesize: 1000,
  594. });
  595. const cycle = li.output ? li.output.data || [] : [];
  596. // 应用版本列表
  597. const appVersion = await getSearchData({
  598. gcode: 'APP_VERSION',
  599. source: defaultAppNameFunc(cycle, 'mname'),
  600. });
  601. // 端列表
  602. const clientList = await getSearchData({
  603. gcode: 'CLIENT_TYPE',
  604. source: defaultAppNameFunc(cycle, 'mname'),
  605. });
  606. // 应用渠道列表
  607. const channel = await getSearchData({
  608. gcode: 'CHANNEL',
  609. source: defaultAppNameFunc(cycle, 'mname'),
  610. });
  611. return { r, li, appVersion, channel, clientList };
  612. },
  613. disabledDate(time) {
  614. const first = new Date('2021-06-21 00:00:00');
  615. return (
  616. time.getTime() > Date.now() - 86400000 ||
  617. time.getTime() < first.getTime()
  618. );
  619. },
  620. FormData(date) {
  621. const d = new Date(date || Date.now() - 86400000);
  622. const year = d.getFullYear();
  623. const month =
  624. d.getMonth() <= 8 ? '0' + (d.getMonth() + 1) : d.getMonth() + 1;
  625. const day = d.getDate() <= 9 ? '0' + d.getDate() : d.getDate();
  626. return [year, month, day].join('-');
  627. },
  628. timeFormat(t) {
  629. const Time = t || 0;
  630. const mH = Time % 3600;
  631. let hour = (Time - mH) / 3600;
  632. let min = (mH - (mH % 60)) / 60;
  633. let son = Number(mH % 60).toFixed(0);
  634. hour = hour <= 9 ? '0' + hour : hour;
  635. min = min <= 9 ? '0' + min : min;
  636. son = son <= 9 ? '0' + son : son;
  637. let out = [];
  638. if (hour * 1 > 0) out.push(hour);
  639. out.push(...[min, son]);
  640. return out.join(':');
  641. },
  642. change() {
  643. const roleid = JSON.parse(
  644. window.parent.localStorage.userinfo || '{}'
  645. ).roleid;
  646. const appV = {
  647. length: 0,
  648. };
  649. // 如果应用选择西部网则渠道默认全部
  650. this.form.app === '西部网' && (this.form.client = -1);
  651. this.getUser();
  652. getRule({
  653. db: 'authplat',
  654. exportMark: '0',
  655. menuid: 399,
  656. roleid,
  657. }).then(rule => {
  658. let prvList =
  659. rule.output && rule.output.data
  660. ? rule.output.data.prvRolectrl || []
  661. : [];
  662. for (let i = 0; i < prvList.length; i++) {
  663. const v = prvList[i];
  664. if (v.controlid == 'APP_VERSION')
  665. (appV[v.detid] = true), (appV.length = appV.length + 1);
  666. }
  667. getSearchData({
  668. gcode: 'APP_VERSION',
  669. source: this.form.app,
  670. }).then(r => {
  671. let version = [
  672. {
  673. label: '全部',
  674. value: -1,
  675. },
  676. ];
  677. r.map(v => {
  678. if ((appV.length && appV[v.mcode]) || appV.length === 0)
  679. version.push({
  680. value: v.mname,
  681. label: v.mname,
  682. });
  683. });
  684. this.version = version;
  685. this.form = {
  686. ...this.form,
  687. version: [-1],
  688. };
  689. });
  690. });
  691. this.onSubmit();
  692. },
  693. changeversion(v) {
  694. if (!v.length) return (this.form.version = [-1]);
  695. const last = v[v.length - 1];
  696. if (last == -1) return (this.form.version = [-1]);
  697. let ver = [];
  698. for (let i = 0; i < v.length; i++) {
  699. const element = v[i];
  700. if (element == -1) continue;
  701. ver.push(element);
  702. }
  703. this.form.version = ver;
  704. },
  705. changechannel(v) {
  706. if (!v.length) return (this.form.channel = [-1]);
  707. const last = v[v.length - 1];
  708. if (last == -1) return (this.form.channel = [-1]);
  709. let ver = [];
  710. for (let i = 0; i < v.length; i++) {
  711. const element = v[i];
  712. if (element == -1) continue;
  713. ver.push(element);
  714. }
  715. this.form.channel = ver;
  716. },
  717. onExport() {
  718. const S = this.form.date[0]
  719. ? this.FormData(this.form.date[0])
  720. : undefined;
  721. const E = this.form.date[1]
  722. ? this.FormData(this.form.date[1])
  723. : undefined;
  724. let p = this.lastParams.app
  725. ? this.lastParams
  726. : {
  727. app: this.form.app,
  728. start: S,
  729. end: E,
  730. manufacturer:
  731. this.form.channel == -1 ? undefined : this.form.channel,
  732. version: this.form.version == -1 ? undefined : this.form.version,
  733. };
  734. getHistory(p).then(r => {
  735. // 生成数据
  736. let strcsv =
  737. 'data:text/csv;charset=utf-8,\uFEFF日期,活跃用户,新增用户,累计用户,人均使用时长,次均使用时长\r\n';
  738. (r.list || []).map(v => {
  739. strcsv += [
  740. v.dt + '\t',
  741. v.activeUser,
  742. v.newUser,
  743. v.totalUser,
  744. this.timeFormat(v.durationUser) + '\t',
  745. this.timeFormat(v.durationTimes) + '\t',
  746. '\r\n',
  747. ].join(',');
  748. });
  749. // 导出
  750. let link = document.createElement('a');
  751. link.id = 'download-csv';
  752. link.setAttribute('href', encodeURI(strcsv));
  753. link.setAttribute(
  754. 'download',
  755. p.app + '用户趋势' + S + '_' + E + '.csv'
  756. );
  757. // document.body.appendChild(link);
  758. link.click();
  759. });
  760. },
  761. },
  762. components: {
  763. countTo,
  764. },
  765. };
  766. </script>
  767. <style>
  768. .History {
  769. margin: 10px 15px;
  770. }
  771. .History .has-seconds .el-time-spinner__wrapper:last-child {
  772. display: none;
  773. }
  774. .head {
  775. display: flex;
  776. font-weight: 500;
  777. }
  778. .head .head-item {
  779. flex: 1;
  780. text-align: center;
  781. font-size: 0.8em;
  782. border-top: 3px solid #fff;
  783. padding-top: 10px;
  784. }
  785. .head .value {
  786. margin: 15px 0;
  787. color: #396fff;
  788. font-size: 25px;
  789. }
  790. </style>