index.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796
  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. };
  431. if (ret[type]) return;
  432. this.type = type;
  433. const keyList = [],
  434. valueList = [];
  435. (this.oriData.list || []).map(v => {
  436. keyList.push(v.dt);
  437. valueList.push(v[this.type]);
  438. });
  439. chart && chart.clear();
  440. this.createImage(keyList, valueList, title);
  441. },
  442. createImage(keyList, valueList, title) {
  443. if (!chart) {
  444. chart = echarts.init(this.$refs.realLineChart);
  445. window.onresize = chart.resize;
  446. }
  447. chart.resize({
  448. height: (this.$refs.realLineChart.offsetWidth * 4) / 16,
  449. });
  450. const _this = this;
  451. chart.setOption({
  452. tooltip: {
  453. confine: true,
  454. trigger: 'axis',
  455. formatter(v) {
  456. const item = v[0] || {};
  457. let val = item.data || 0;
  458. if (/duration/.test(_this.type)) val = _this.timeFormat(val);
  459. return item.axisValue + '<br />' + title + ':' + val;
  460. },
  461. },
  462. toolbox: {
  463. feature: {
  464. saveAsImage: {
  465. type: 'jpg',
  466. name: '趋势',
  467. },
  468. },
  469. },
  470. legend: {
  471. data: [title],
  472. },
  473. grid: {
  474. left: '3%',
  475. right: '4%',
  476. bottom: '3%',
  477. containLabel: true,
  478. },
  479. xAxis: {
  480. type: 'category',
  481. boundaryGap: true,
  482. data: keyList,
  483. },
  484. yAxis: {
  485. type: 'value',
  486. scale: true,
  487. minInterval: 1,
  488. axisLabel: {
  489. formatter(v) {
  490. let val = v;
  491. if (_this.type === 'duration') val = _this.timeFormat(val);
  492. else {
  493. if (val >= 100000000) {
  494. val = (val / 100000000).toFixed(2) + '亿';
  495. } else if (val >= 10000) {
  496. val = (val / 10000).toFixed(2) + '万';
  497. }
  498. }
  499. return val;
  500. },
  501. },
  502. },
  503. series: [
  504. {
  505. name: title,
  506. data: valueList,
  507. symbolSize: 0,
  508. lineStyle: {
  509. width: 1,
  510. },
  511. type: 'line',
  512. smooth: true,
  513. color: 'rgba(58,132,255,.9)',
  514. areaStyle: {
  515. color: {
  516. type: 'linear',
  517. x: 0,
  518. y: 0,
  519. x2: 0,
  520. y2: 1,
  521. colorStops: [
  522. {
  523. offset: 0,
  524. color: 'rgba(58,132,255, 0.8)', // 0% 处的颜色
  525. },
  526. {
  527. offset: 1,
  528. color: 'rgba(58,132,255, 0.1)', // 100% 处的颜色
  529. },
  530. ],
  531. global: false, // 缺省为 false
  532. },
  533. },
  534. },
  535. ],
  536. });
  537. },
  538. async getAppListFunc() {
  539. const { r, li, appVersion, channel, clientList } =
  540. await this.getAppListOri();
  541. let source = { length: 0 },
  542. appli = [];
  543. let appV = { length: 0 },
  544. appVLi = [];
  545. let appC = { length: 0 },
  546. appCLi = [];
  547. let clentV = { length: 0 },
  548. clentli = [];
  549. let prvList =
  550. r.output && r.output.data ? r.output.data.prvRolectrl || [] : [];
  551. for (let i = 0; i < prvList.length; i++) {
  552. const v = prvList[i];
  553. if (v.controlid == 'RMT_SOURCE')
  554. (source[v.detid] = true), (source.length = source.length + 1);
  555. if (v.controlid == 'APP_VERSION')
  556. (appV[v.detid] = true), (appV.length = appV.length + 1);
  557. if (v.controlid == 'CHANNEL')
  558. (appC[v.detid] = true), (appC.length = appC.length + 1);
  559. if (v.controlid == 'CLIENT_TYPE')
  560. (clentV[v.detid] = true), (clentV.length = clentV.length + 1);
  561. }
  562. if (li.status === '0') appli = li.output.data || [];
  563. if (appVersion.length) appVLi = appVersion || [];
  564. if (channel.length) appCLi = channel || [];
  565. if (clientList.length) clentli = clientList || [];
  566. return {
  567. source,
  568. appV,
  569. appC,
  570. appli,
  571. clentV,
  572. appVLi,
  573. appCLi,
  574. clentli,
  575. };
  576. },
  577. async getAppListOri() {
  578. const roleid = JSON.parse(
  579. window.parent.localStorage.userinfo || '{}'
  580. ).roleid;
  581. const r = await getRule({
  582. db: 'authplat',
  583. exportMark: '0',
  584. menuid: 399,
  585. roleid,
  586. });
  587. // 应用列表
  588. const li = await getAppList({
  589. exportMark: '0',
  590. gcode: 'SOURCE',
  591. pageid: 1,
  592. pagesize: 1000,
  593. });
  594. const cycle = li.output ? li.output.data || [] : [];
  595. // 应用版本列表
  596. const appVersion = await getSearchData({
  597. gcode: 'APP_VERSION',
  598. source: defaultAppNameFunc(cycle, 'mname'),
  599. });
  600. // 端列表
  601. const clientList = await getSearchData({
  602. gcode: 'CLIENT_TYPE',
  603. source: defaultAppNameFunc(cycle, 'mname'),
  604. });
  605. // 应用渠道列表
  606. const channel = await getSearchData({
  607. gcode: 'CHANNEL',
  608. source: defaultAppNameFunc(cycle, 'mname'),
  609. });
  610. return { r, li, appVersion, channel, clientList };
  611. },
  612. disabledDate(time) {
  613. const first = new Date('2021-06-21 00:00:00');
  614. return (
  615. time.getTime() > Date.now() - 86400000 ||
  616. time.getTime() < first.getTime()
  617. );
  618. },
  619. FormData(date) {
  620. const d = new Date(date || Date.now() - 86400000);
  621. const year = d.getFullYear();
  622. const month =
  623. d.getMonth() <= 8 ? '0' + (d.getMonth() + 1) : d.getMonth() + 1;
  624. const day = d.getDate() <= 9 ? '0' + d.getDate() : d.getDate();
  625. return [year, month, day].join('-');
  626. },
  627. timeFormat(t) {
  628. const Time = t || 0;
  629. const mH = Time % 3600;
  630. let hour = (Time - mH) / 3600;
  631. let min = (mH - (mH % 60)) / 60;
  632. let son = Number(mH % 60).toFixed(0);
  633. hour = hour <= 9 ? '0' + hour : hour;
  634. min = min <= 9 ? '0' + min : min;
  635. son = son <= 9 ? '0' + son : son;
  636. let out = [];
  637. if (hour * 1 > 0) out.push(hour);
  638. out.push(...[min, son]);
  639. return out.join(':');
  640. },
  641. change() {
  642. const roleid = JSON.parse(
  643. window.parent.localStorage.userinfo || '{}'
  644. ).roleid;
  645. const appV = {
  646. length: 0,
  647. };
  648. // 如果应用选择西部网则渠道默认全部
  649. this.form.app === '西部网' && (this.form.client = -1);
  650. this.getUser();
  651. getRule({
  652. db: 'authplat',
  653. exportMark: '0',
  654. menuid: 399,
  655. roleid,
  656. }).then(rule => {
  657. let prvList =
  658. rule.output && rule.output.data
  659. ? rule.output.data.prvRolectrl || []
  660. : [];
  661. for (let i = 0; i < prvList.length; i++) {
  662. const v = prvList[i];
  663. if (v.controlid == 'APP_VERSION')
  664. (appV[v.detid] = true), (appV.length = appV.length + 1);
  665. }
  666. getSearchData({
  667. gcode: 'APP_VERSION',
  668. source: this.form.app,
  669. }).then(r => {
  670. let version = [
  671. {
  672. label: '全部',
  673. value: -1,
  674. },
  675. ];
  676. r.map(v => {
  677. if ((appV.length && appV[v.mcode]) || appV.length === 0)
  678. version.push({
  679. value: v.mname,
  680. label: v.mname,
  681. });
  682. });
  683. this.version = version;
  684. this.form = {
  685. ...this.form,
  686. version: [-1],
  687. };
  688. });
  689. });
  690. },
  691. changeversion(v) {
  692. if (!v.length) return (this.form.version = [-1]);
  693. const last = v[v.length - 1];
  694. if (last == -1) return (this.form.version = [-1]);
  695. let ver = [];
  696. for (let i = 0; i < v.length; i++) {
  697. const element = v[i];
  698. if (element == -1) continue;
  699. ver.push(element);
  700. }
  701. this.form.version = ver;
  702. },
  703. changechannel(v) {
  704. if (!v.length) return (this.form.channel = [-1]);
  705. const last = v[v.length - 1];
  706. if (last == -1) return (this.form.channel = [-1]);
  707. let ver = [];
  708. for (let i = 0; i < v.length; i++) {
  709. const element = v[i];
  710. if (element == -1) continue;
  711. ver.push(element);
  712. }
  713. this.form.channel = ver;
  714. },
  715. onExport() {
  716. const S = this.form.date[0]
  717. ? this.FormData(this.form.date[0])
  718. : undefined;
  719. const E = this.form.date[1]
  720. ? this.FormData(this.form.date[1])
  721. : undefined;
  722. let p = this.lastParams.app
  723. ? this.lastParams
  724. : {
  725. app: this.form.app,
  726. start: S,
  727. end: E,
  728. manufacturer:
  729. this.form.channel == -1 ? undefined : this.form.channel,
  730. version: this.form.version == -1 ? undefined : this.form.version,
  731. };
  732. getHistory(p).then(r => {
  733. // 生成数据
  734. let strcsv =
  735. 'data:text/csv;charset=utf-8,\uFEFF日期,活跃用户,新增用户,累计用户,人均使用时长,次均使用时长\r\n';
  736. (r.list || []).map(v => {
  737. strcsv += [
  738. v.dt + '\t',
  739. v.activeUser,
  740. v.newUser,
  741. v.totalUser,
  742. this.timeFormat(v.durationUser) + '\t',
  743. this.timeFormat(v.durationTimes) + '\t',
  744. '\r\n',
  745. ].join(',');
  746. });
  747. // 导出
  748. let link = document.createElement('a');
  749. link.id = 'download-csv';
  750. link.setAttribute('href', encodeURI(strcsv));
  751. link.setAttribute(
  752. 'download',
  753. p.app + '用户趋势' + S + '_' + E + '.csv'
  754. );
  755. // document.body.appendChild(link);
  756. link.click();
  757. });
  758. },
  759. },
  760. components: {
  761. countTo,
  762. },
  763. };
  764. </script>
  765. <style>
  766. .History {
  767. margin: 10px 15px;
  768. }
  769. .History .has-seconds .el-time-spinner__wrapper:last-child {
  770. display: none;
  771. }
  772. .head {
  773. display: flex;
  774. font-weight: 500;
  775. }
  776. .head .head-item {
  777. flex: 1;
  778. text-align: center;
  779. font-size: 0.8em;
  780. border-top: 3px solid #fff;
  781. padding-top: 10px;
  782. }
  783. .head .value {
  784. margin: 15px 0;
  785. color: #396fff;
  786. font-size: 25px;
  787. }
  788. </style>