index.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832
  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. },
  321. computed: {},
  322. methods: {
  323. getUser() {
  324. let app = this.form.app;
  325. if (!app) return;
  326. if (this.form.client == '999') app = '起点新闻极速版';
  327. getUserTotal({ app }).then(r => {
  328. this.userTotalList = (r || []).map(v => {
  329. return {
  330. name: v.name,
  331. value: v.value,
  332. key: v.name,
  333. isNum: isNaN(v.value)
  334. };
  335. });
  336. });
  337. },
  338. verifyList(list, verify, obj, more) {
  339. if (!obj) return;
  340. let li = list || [];
  341. const out = [];
  342. more && out.push({ value: -1, label: '全部' });
  343. for (let i = 0; i < li.length; i++) {
  344. const v = li[i];
  345. if (verify.length !== 0 && !verify[v.mcode]) continue;
  346. out.push({
  347. value: v[obj.value],
  348. label: v[obj.label]
  349. });
  350. }
  351. return out;
  352. },
  353. pagechange(p) {
  354. this.page = p;
  355. this.pushShowList();
  356. },
  357. pushShowList() {
  358. let s = this.page - 1 < 0 ? 0 : (this.page - 1) * 10;
  359. let e = this.page * 10;
  360. let li = JSON.parse(JSON.stringify(this.oriData.list || []));
  361. let out = [];
  362. for (let i = s; i < e; i++) {
  363. li[i] && out.push(li[i]);
  364. }
  365. this.showList = out;
  366. },
  367. onSubmit() {
  368. this.lastParams = {
  369. app: this.form.app,
  370. start: this.FormData(this.form.date[0]),
  371. end: this.FormData(this.form.date[1]),
  372. manufacturer: this.form.channel == -1 ? undefined : this.form.channel,
  373. version: this.form.version == -1 ? undefined : this.form.version,
  374. lib: this.form.client == -1 ? undefined : this.form.client
  375. };
  376. this.getUser();
  377. getHistory(this.lastParams)
  378. .then(r => {
  379. if (!this.$refs.realLineChart) return;
  380. let oriData = r || {};
  381. const total = oriData.total || {},
  382. keys = Object.keys(total),
  383. color = ['rgb(244, 127, 146)', 'rgb(17, 160, 248)'],
  384. p = [];
  385. let tab = undefined,
  386. tabName = undefined;
  387. for (let i = 0; i < keys.length; i++) {
  388. const v = keys[i];
  389. if (!total[v].value && total[v].value !== 0) continue;
  390. let isNum = isNaN(total[v].value);
  391. let value = '';
  392. if (
  393. isNum &&
  394. !/次数/g.test(total[v].name) &&
  395. !/下载量/g.test(total[v].name) &&
  396. !/装机量/g.test(total[v].name) &&
  397. !/用户数/g.test(total[v].name) &&
  398. typeof total[v].value === 'number'
  399. )
  400. value = this.timeFormat(total[v].value);
  401. else value = total[v].value;
  402. if (!tab && v !== 'downloads') {
  403. tab = v;
  404. tabName = total[v].name;
  405. }
  406. p.push({
  407. name: total[v].name,
  408. value,
  409. color: color[i % 2],
  410. key: v,
  411. isNum
  412. });
  413. }
  414. this.oriData = {
  415. list: oriData.list || [],
  416. total: p
  417. };
  418. this.changeData(tab, tabName);
  419. chart.hideLoading();
  420. this.page = 1;
  421. this.pushShowList();
  422. })
  423. .catch(() => {
  424. this.oriData = {};
  425. this.pushShowList();
  426. chart && chart.clear() && chart.hideLoading();
  427. });
  428. },
  429. changeData(type, title) {
  430. const ret = {
  431. downloads: true,
  432. ip: true,
  433. avgUser: true
  434. };
  435. if (ret[type]) return;
  436. this.type = type;
  437. const keyList = [],
  438. valueList = [];
  439. (this.oriData.list || []).map(v => {
  440. keyList.push(v.dt);
  441. valueList.push(v[this.type]);
  442. });
  443. chart && chart.clear();
  444. this.createImage(keyList, valueList, title);
  445. },
  446. createImage(keyList, valueList, title) {
  447. if (!chart) {
  448. chart = echarts.init(this.$refs.realLineChart);
  449. window.onresize = chart.resize;
  450. }
  451. chart.resize({
  452. height: (this.$refs.realLineChart.offsetWidth * 4) / 16
  453. });
  454. const _this = this;
  455. chart.setOption({
  456. tooltip: {
  457. confine: true,
  458. trigger: 'axis',
  459. formatter(v) {
  460. const item = v[0] || {};
  461. let val = item.data || 0;
  462. if (/duration/.test(_this.type)) val = _this.timeFormat(val);
  463. return item.axisValue + '<br />' + title + ':' + val;
  464. }
  465. },
  466. toolbox: {
  467. feature: {
  468. saveAsImage: {
  469. type: 'jpg',
  470. name: '趋势'
  471. }
  472. }
  473. },
  474. legend: {
  475. data: [title]
  476. },
  477. grid: {
  478. left: '3%',
  479. right: '4%',
  480. bottom: '3%',
  481. containLabel: true
  482. },
  483. xAxis: {
  484. type: 'category',
  485. boundaryGap: true,
  486. data: keyList
  487. },
  488. yAxis: {
  489. type: 'value',
  490. scale: true,
  491. minInterval: 1,
  492. axisLabel: {
  493. formatter(v) {
  494. let val = v;
  495. if (_this.type === 'duration') val = _this.timeFormat(val);
  496. else {
  497. if (val >= 100000000) {
  498. val = (val / 100000000).toFixed(2) + '亿';
  499. } else if (val >= 10000) {
  500. val = (val / 10000).toFixed(2) + '万';
  501. }
  502. }
  503. return val;
  504. }
  505. }
  506. },
  507. series: [
  508. {
  509. name: title,
  510. data: valueList,
  511. symbolSize: 0,
  512. lineStyle: {
  513. width: 1
  514. },
  515. type: 'line',
  516. smooth: true,
  517. color: 'rgba(58,132,255,.9)',
  518. areaStyle: {
  519. color: {
  520. type: 'linear',
  521. x: 0,
  522. y: 0,
  523. x2: 0,
  524. y2: 1,
  525. colorStops: [
  526. {
  527. offset: 0,
  528. color: 'rgba(58,132,255, 0.8)' // 0% 处的颜色
  529. },
  530. {
  531. offset: 1,
  532. color: 'rgba(58,132,255, 0.1)' // 100% 处的颜色
  533. }
  534. ],
  535. global: false // 缺省为 false
  536. }
  537. }
  538. }
  539. ]
  540. });
  541. },
  542. async getAppListFunc() {
  543. const { r, li, appVersion, channel, clientList } =
  544. await this.getAppListOri();
  545. let source = { length: 0 },
  546. appli = [];
  547. let appV = { length: 0 },
  548. appVLi = [];
  549. let appC = { length: 0 },
  550. appCLi = [];
  551. let clentV = { length: 0 },
  552. clentli = [];
  553. let prvList =
  554. r.output && r.output.data ? r.output.data.prvRolectrl || [] : [];
  555. for (let i = 0; i < prvList.length; i++) {
  556. const v = prvList[i];
  557. if (v.controlid == 'RMT_SOURCE')
  558. (source[v.detid] = true), (source.length = source.length + 1);
  559. if (v.controlid == 'APP_VERSION')
  560. (appV[v.detid] = true), (appV.length = appV.length + 1);
  561. if (v.controlid == 'CHANNEL')
  562. (appC[v.detid] = true), (appC.length = appC.length + 1);
  563. if (v.controlid == 'CLIENT_TYPE')
  564. (clentV[v.detid] = true), (clentV.length = clentV.length + 1);
  565. }
  566. if (li.status === '0') appli = li.output.data || [];
  567. if (appVersion.length) appVLi = appVersion || [];
  568. if (channel.length) appCLi = channel || [];
  569. if (clientList.length) clentli = clientList || [];
  570. return {
  571. source,
  572. appV,
  573. appC,
  574. appli,
  575. clentV,
  576. appVLi,
  577. appCLi,
  578. clentli
  579. };
  580. },
  581. async getAppListOri() {
  582. const roleid = JSON.parse(
  583. window.parent.localStorage.userinfo || '{}'
  584. ).roleid;
  585. const r = await getRule({
  586. db: 'authplat',
  587. exportMark: '0',
  588. menuid: 399,
  589. roleid
  590. });
  591. // 应用列表
  592. const li = await getAppList({
  593. exportMark: '0',
  594. gcode: 'SOURCE',
  595. pageid: 1,
  596. pagesize: 1000
  597. });
  598. const cycle = li.output ? li.output.data || [] : [];
  599. // 应用版本列表
  600. const appVersion = await getSearchData({
  601. gcode: 'APP_VERSION',
  602. source: defaultAppNameFunc(cycle, 'mname')
  603. });
  604. // 端列表
  605. const clientList = await getSearchData({
  606. gcode: 'CLIENT_TYPE',
  607. source: defaultAppNameFunc(cycle, 'mname')
  608. });
  609. // 应用渠道列表
  610. const channel = await getSearchData({
  611. gcode: 'CHANNEL',
  612. source: defaultAppNameFunc(cycle, 'mname')
  613. });
  614. return { r, li, appVersion, channel, clientList };
  615. },
  616. disabledDate(time) {
  617. const first = new Date('2021-06-21 00:00:00');
  618. return (
  619. time.getTime() > Date.now() - 86400000 ||
  620. time.getTime() < first.getTime()
  621. );
  622. },
  623. FormData(date) {
  624. const d = new Date(date || Date.now() - 86400000);
  625. const year = d.getFullYear();
  626. const month =
  627. d.getMonth() <= 8 ? '0' + (d.getMonth() + 1) : d.getMonth() + 1;
  628. const day = d.getDate() <= 9 ? '0' + d.getDate() : d.getDate();
  629. return [year, month, day].join('-');
  630. },
  631. timeFormat(t) {
  632. const Time = t || 0;
  633. const mH = Time % 3600;
  634. let hour = (Time - mH) / 3600;
  635. let min = (mH - (mH % 60)) / 60;
  636. let son = Number(mH % 60).toFixed(0);
  637. hour = hour <= 9 ? '0' + hour : hour;
  638. min = min <= 9 ? '0' + min : min;
  639. son = son <= 9 ? '0' + son : son;
  640. let out = [];
  641. if (hour * 1 > 0) out.push(hour);
  642. out.push(...[min, son]);
  643. return out.join(':');
  644. },
  645. async change() {
  646. const roleid = JSON.parse(
  647. window.parent.localStorage.userinfo || '{}'
  648. ).roleid;
  649. const appV = {
  650. length: 0
  651. };
  652. // 端列表
  653. const r = await getRule({
  654. db: 'authplat',
  655. exportMark: '0',
  656. menuid: 399,
  657. roleid
  658. });
  659. let prvList =
  660. r.output && r.output.data ? r.output.data.prvRolectrl || [] : [];
  661. const clientList =
  662. (await getSearchData({
  663. gcode: 'CLIENT_TYPE',
  664. source: this.form.app
  665. })) || [];
  666. let clentV = { length: 0 };
  667. for (let i = 0; i < prvList.length; i++) {
  668. const v = prvList[i];
  669. if (v.controlid == 'CLIENT_TYPE')
  670. (clentV[v.detid] = true), (clentV.length = clentV.length + 1);
  671. }
  672. this.client = this.verifyList(
  673. clientList,
  674. clentV,
  675. {
  676. value: 'mcode',
  677. label: 'mname'
  678. },
  679. true
  680. );
  681. this.form.client = this.client[1].value;
  682. // 如果应用选择西部网则渠道默认全部
  683. this.form.app === '西部网' && (this.form.client = -1);
  684. this.getUser();
  685. getRule({
  686. db: 'authplat',
  687. exportMark: '0',
  688. menuid: 399,
  689. roleid
  690. }).then(rule => {
  691. let prvList =
  692. rule.output && rule.output.data
  693. ? rule.output.data.prvRolectrl || []
  694. : [];
  695. for (let i = 0; i < prvList.length; i++) {
  696. const v = prvList[i];
  697. if (v.controlid == 'APP_VERSION')
  698. (appV[v.detid] = true), (appV.length = appV.length + 1);
  699. }
  700. getSearchData({
  701. gcode: 'APP_VERSION',
  702. source: this.form.app
  703. }).then(r => {
  704. let version = [
  705. {
  706. label: '全部',
  707. value: -1
  708. }
  709. ];
  710. r.map(v => {
  711. if ((appV.length && appV[v.mcode]) || appV.length === 0)
  712. version.push({
  713. value: v.mname,
  714. label: v.mname
  715. });
  716. });
  717. this.version = version;
  718. this.form = {
  719. ...this.form,
  720. version: [-1]
  721. };
  722. });
  723. });
  724. this.onSubmit();
  725. },
  726. changeversion(v) {
  727. if (!v.length) return (this.form.version = [-1]);
  728. const last = v[v.length - 1];
  729. if (last == -1) return (this.form.version = [-1]);
  730. let ver = [];
  731. for (let i = 0; i < v.length; i++) {
  732. const element = v[i];
  733. if (element == -1) continue;
  734. ver.push(element);
  735. }
  736. this.form.version = ver;
  737. },
  738. changechannel(v) {
  739. if (!v.length) return (this.form.channel = [-1]);
  740. const last = v[v.length - 1];
  741. if (last == -1) return (this.form.channel = [-1]);
  742. let ver = [];
  743. for (let i = 0; i < v.length; i++) {
  744. const element = v[i];
  745. if (element == -1) continue;
  746. ver.push(element);
  747. }
  748. this.form.channel = ver;
  749. },
  750. onExport() {
  751. const S = this.form.date[0]
  752. ? this.FormData(this.form.date[0])
  753. : undefined;
  754. const E = this.form.date[1]
  755. ? this.FormData(this.form.date[1])
  756. : undefined;
  757. let p = this.lastParams.app
  758. ? this.lastParams
  759. : {
  760. app: this.form.app,
  761. start: S,
  762. end: E,
  763. manufacturer:
  764. this.form.channel == -1 ? undefined : this.form.channel,
  765. version: this.form.version == -1 ? undefined : this.form.version
  766. };
  767. getHistory(p).then(r => {
  768. // 生成数据
  769. let strcsv =
  770. 'data:text/csv;charset=utf-8,\uFEFF日期,活跃用户,新增用户,累计用户,人均使用时长,次均使用时长\r\n';
  771. (r.list || []).map(v => {
  772. strcsv += [
  773. v.dt + '\t',
  774. v.activeUser,
  775. v.newUser,
  776. v.totalUser,
  777. this.timeFormat(v.durationUser) + '\t',
  778. this.timeFormat(v.durationTimes) + '\t',
  779. '\r\n'
  780. ].join(',');
  781. });
  782. // 导出
  783. let link = document.createElement('a');
  784. link.id = 'download-csv';
  785. link.setAttribute('href', encodeURI(strcsv));
  786. link.setAttribute(
  787. 'download',
  788. p.app + '用户趋势' + S + '_' + E + '.csv'
  789. );
  790. // document.body.appendChild(link);
  791. link.click();
  792. });
  793. }
  794. },
  795. components: {
  796. countTo
  797. }
  798. };
  799. </script>
  800. <style>
  801. .History {
  802. margin: 10px 15px;
  803. }
  804. .History .has-seconds .el-time-spinner__wrapper:last-child {
  805. display: none;
  806. }
  807. .head {
  808. display: flex;
  809. font-weight: 500;
  810. }
  811. .head .head-item {
  812. flex: 1;
  813. text-align: center;
  814. font-size: 0.8em;
  815. border-top: 3px solid #fff;
  816. padding-top: 10px;
  817. }
  818. .head .value {
  819. margin: 15px 0;
  820. color: #396fff;
  821. font-size: 25px;
  822. }
  823. </style>