index.vue 19 KB

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