index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  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. <el-table
  93. v-if="this.oriData && this.oriData.list && this.oriData.list.length"
  94. :data="showList"
  95. style="width: 100%"
  96. :header-cell-style="{ backgroundColor: '#f4f5f7', color: '#606266' }"
  97. >
  98. <el-table-column prop="dt" label="日期" />
  99. <el-table-column prop="activeUser" label="活跃用户">
  100. <template #default="scope">
  101. <countTo
  102. :startVal="scope.row.activeUser"
  103. :endVal="scope.row.activeUser"
  104. :duration="100"
  105. ></countTo>
  106. </template>
  107. </el-table-column>
  108. <el-table-column prop="newUser" label="新增用户">
  109. <template #default="scope">
  110. <countTo
  111. :startVal="scope.row.newUser"
  112. :endVal="scope.row.newUser"
  113. :duration="100"
  114. ></countTo>
  115. </template>
  116. </el-table-column>
  117. <el-table-column prop="totalUser" label="累计用户">
  118. <template #default="scope">
  119. <countTo
  120. :startVal="scope.row.totalUser"
  121. :endVal="scope.row.totalUser"
  122. :duration="100"
  123. ></countTo>
  124. </template>
  125. </el-table-column>
  126. <el-table-column prop="startTimes" label="启动次数">
  127. <template #default="scope">
  128. <countTo
  129. :startVal="scope.row.startTimes"
  130. :endVal="scope.row.startTimes"
  131. :duration="100"
  132. ></countTo>
  133. </template>
  134. </el-table-column>
  135. <el-table-column prop="duration" label="人均使用时长">
  136. <template #default="scope">
  137. {{ timeFormat(scope.row.durationUser) }}
  138. </template>
  139. </el-table-column>
  140. <el-table-column prop="duration" label="次均使用时长">
  141. <template #default="scope">
  142. {{ timeFormat(scope.row.durationTimes) }}
  143. </template>
  144. </el-table-column>
  145. </el-table>
  146. <div v-if="oriData.list && oriData.list.length">
  147. <el-pagination
  148. v-if="Math.ceil(oriData.list.length / 10) > 1"
  149. :current-page="page"
  150. layout="prev, pager, next"
  151. :total="oriData.list.length"
  152. @current-change="pagechange"
  153. />
  154. </div>
  155. </el-card>
  156. </div>
  157. </template>
  158. <script>
  159. // @ is an alias to /src
  160. import { getRule, getAppList, getHistory, getSearchData } from "@/api/index";
  161. import countTo from "@/components/counto/vue-countTo.vue";
  162. // import config from "@/config/index";
  163. export default {
  164. name: "RealOnline",
  165. data() {
  166. return {
  167. type: "",
  168. lastParams: {},
  169. page: 1,
  170. form: {
  171. app: "",
  172. version: [],
  173. channel: [],
  174. date: [],
  175. },
  176. cycle: [],
  177. oriData: {},
  178. showList: [],
  179. version: [],
  180. channel: [],
  181. };
  182. },
  183. async mounted() {
  184. const { source, appV, appC, appli, appCLi, appVLi } =
  185. await this.getAppListFunc();
  186. const keys = {
  187. value: "mname",
  188. label: "mname",
  189. };
  190. this.cycle = this.verifyList(appli, source, keys, false);
  191. this.channel = this.verifyList(appCLi, appC, keys, true);
  192. this.version = this.verifyList(appVLi, appV, keys, true);
  193. this.form = {
  194. // app: (this.cycle[0] || { value: "" }).value,
  195. app: "起点新闻",
  196. version: [(this.version[0] || { value: "" }).value],
  197. channel: [(this.channel[0] || { value: "" }).value],
  198. date: [new Date(Date.now() - 604800000), new Date(Date.now() - 86400000)],
  199. };
  200. this.onSubmit();
  201. },
  202. computed: {},
  203. methods: {
  204. verifyList(list, verify, obj, more) {
  205. if (!obj) return;
  206. let li = list || [];
  207. const out = [];
  208. more && out.push({ value: -1, label: "不限" });
  209. for (let i = 0; i < li.length; i++) {
  210. const v = li[i];
  211. if (verify.length !== 0 && !verify[v.mcode]) continue;
  212. out.push({
  213. value: v[obj.value],
  214. label: v[obj.label],
  215. });
  216. }
  217. return out;
  218. },
  219. pagechange(p) {
  220. this.page = p;
  221. this.pushShowList();
  222. },
  223. pushShowList() {
  224. let s = this.page - 1 < 0 ? 0 : (this.page - 1) * 10;
  225. let e = this.page * 10;
  226. let li = JSON.parse(JSON.stringify(this.oriData.list || []));
  227. let out = [];
  228. for (let i = s; i < e; i++) {
  229. li[i] && out.push(li[i]);
  230. }
  231. this.showList = out;
  232. },
  233. onSubmit() {
  234. this.lastParams = {
  235. app: this.form.app,
  236. start: this.FormData(this.form.date[0]),
  237. end: this.FormData(this.form.date[1]),
  238. manufacturer: this.form.channel == -1 ? undefined : this.form.channel,
  239. version: this.form.version == -1 ? undefined : this.form.version,
  240. };
  241. getHistory(this.lastParams)
  242. .then(r => {
  243. let oriData = r || {};
  244. this.oriData = {
  245. list: oriData.list || [],
  246. };
  247. this.page = 1;
  248. this.pushShowList();
  249. })
  250. .catch(() => {
  251. this.oriData = {};
  252. this.pushShowList();
  253. });
  254. },
  255. async getAppListFunc() {
  256. const { r, li, appVersion, channel } = await this.getAppListOri();
  257. let source = { length: 0 },
  258. appli = [];
  259. let appV = { length: 0 },
  260. appVLi = [];
  261. let appC = { length: 0 },
  262. appCLi = [];
  263. let clentV = { length: 0 },
  264. clentli = [];
  265. let prvList = r.output.data.prvRolectrl || [];
  266. for (let i = 0; i < prvList.length; i++) {
  267. const v = prvList[i];
  268. if (v.controlid == "RMT_SOURCE")
  269. (source[v.detid] = true), (source.length = source.length + 1);
  270. if (v.controlid == "APP_VERSION")
  271. (appV[v.detid] = true), (appV.length = appV.length + 1);
  272. if (v.controlid == "CHANNEL")
  273. (appC[v.detid] = true), (appC.length = appC.length + 1);
  274. }
  275. if (li.status === "0") appli = li.output.data || [];
  276. if (appVersion.length) appVLi = appVersion || [];
  277. if (channel.length) appCLi = channel || [];
  278. return {
  279. source,
  280. appV,
  281. appC,
  282. appli,
  283. clentV,
  284. appVLi,
  285. appCLi,
  286. clentli,
  287. };
  288. },
  289. async getAppListOri() {
  290. const roleid = JSON.parse(
  291. window.parent.localStorage.userinfo || "{}"
  292. ).roleid;
  293. const r = await getRule({
  294. db: "authplat",
  295. exportMark: "0",
  296. menuid: 399,
  297. roleid,
  298. });
  299. // 应用列表
  300. const li = await getAppList({
  301. exportMark: "0",
  302. gcode: "SOURCE",
  303. pageid: 1,
  304. pagesize: 1000,
  305. });
  306. const defaultAppName = "起点新闻";
  307. // 应用版本列表
  308. const appVersion = await getSearchData({
  309. gcode: "APP_VERSION",
  310. source: defaultAppName,
  311. });
  312. // 应用渠道列表
  313. const channel = await getSearchData({
  314. gcode: "CHANNEL",
  315. source: defaultAppName,
  316. });
  317. return { r, li, appVersion, channel };
  318. },
  319. disabledDate(time) {
  320. const first = new Date("2021-06-21 00:00:00");
  321. return (
  322. time.getTime() > Date.now() - 86400000 ||
  323. time.getTime() < first.getTime()
  324. );
  325. },
  326. FormData(date) {
  327. const d = new Date(date || Date.now() - 86400000);
  328. const year = d.getFullYear();
  329. const month =
  330. d.getMonth() <= 8 ? "0" + (d.getMonth() + 1) : d.getMonth() + 1;
  331. const day = d.getDate() <= 9 ? "0" + d.getDate() : d.getDate();
  332. return [year, month, day].join("-");
  333. },
  334. timeFormat(t) {
  335. const Time = t || 0;
  336. const mH = Time % 3600;
  337. let hour = (Time - mH) / 3600;
  338. let min = (mH - (mH % 60)) / 60;
  339. let son = Number(mH % 60).toFixed(0);
  340. hour = hour <= 9 ? "0" + hour : hour;
  341. min = min <= 9 ? "0" + min : min;
  342. son = son <= 9 ? "0" + son : son;
  343. let out = [];
  344. if (hour * 1 > 0) out.push(hour);
  345. out.push(...[min, son]);
  346. return out.join(":");
  347. },
  348. change() {
  349. const roleid = JSON.parse(
  350. window.parent.localStorage.userinfo || "{}"
  351. ).roleid;
  352. const appV = {
  353. length: 0,
  354. };
  355. getRule({
  356. db: "authplat",
  357. exportMark: "0",
  358. menuid: 399,
  359. roleid,
  360. }).then(rule => {
  361. let prvList = rule.output.data.prvRolectrl || [];
  362. for (let i = 0; i < prvList.length; i++) {
  363. const v = prvList[i];
  364. if (v.controlid == "APP_VERSION")
  365. (appV[v.detid] = true), (appV.length = appV.length + 1);
  366. }
  367. getSearchData({
  368. gcode: "APP_VERSION",
  369. source: this.form.app,
  370. }).then(r => {
  371. let version = [
  372. {
  373. label: "不限",
  374. value: -1,
  375. },
  376. ];
  377. r.map(v => {
  378. if ((appV.length && appV[v.mcode]) || appV.length === 0)
  379. version.push({
  380. value: v.mname,
  381. label: v.mname,
  382. });
  383. });
  384. this.version = version;
  385. this.form = {
  386. ...this.form,
  387. version: [-1],
  388. };
  389. });
  390. });
  391. },
  392. changeversion(v) {
  393. if (!v.length) return (this.form.version = [-1]);
  394. const last = v[v.length - 1];
  395. if (last == -1) return (this.form.version = [-1]);
  396. let ver = [];
  397. for (let i = 0; i < v.length; i++) {
  398. const element = v[i];
  399. if (element == -1) continue;
  400. ver.push(element);
  401. }
  402. this.form.version = ver;
  403. },
  404. changechannel(v) {
  405. if (!v.length) return (this.form.channel = [-1]);
  406. const last = v[v.length - 1];
  407. if (last == -1) return (this.form.channel = [-1]);
  408. let ver = [];
  409. for (let i = 0; i < v.length; i++) {
  410. const element = v[i];
  411. if (element == -1) continue;
  412. ver.push(element);
  413. }
  414. this.form.channel = ver;
  415. },
  416. onExport() {
  417. const S = this.form.date[0]
  418. ? this.FormData(this.form.date[0])
  419. : undefined;
  420. const E = this.form.date[1]
  421. ? this.FormData(this.form.date[1])
  422. : undefined;
  423. let p = this.lastParams.app
  424. ? this.lastParams
  425. : {
  426. app: this.form.app,
  427. start: S,
  428. end: E,
  429. manufacturer:
  430. this.form.channel == -1 ? undefined : this.form.channel,
  431. version: this.form.version == -1 ? undefined : this.form.version,
  432. };
  433. getHistory(p).then(r => {
  434. // 生成数据
  435. let strcsv =
  436. "data:text/csv;charset=utf-8,日期,活跃用户,新增用户,累计用户,人均使用时长,次均使用时长\r\n";
  437. (r.list || []).map(v => {
  438. strcsv += [
  439. v.dt,
  440. v.activeUser,
  441. v.newUser,
  442. v.totalUser,
  443. this.timeFormat(v.durationUser),
  444. this.timeFormat(v.durationTimes),
  445. "\r\n",
  446. ].join(",");
  447. });
  448. // 导出
  449. let link = document.createElement("a");
  450. link.id = "download-csv";
  451. link.setAttribute("href", encodeURI(strcsv));
  452. link.setAttribute(
  453. "download",
  454. p.app + "用户趋势" + S + "_" + E + ".csv"
  455. );
  456. // document.body.appendChild(link);
  457. link.click();
  458. });
  459. },
  460. },
  461. components: {
  462. countTo,
  463. },
  464. };
  465. </script>
  466. <style>
  467. .RealOnline {
  468. margin: 10px 15px;
  469. }
  470. .RealOnline .has-seconds .el-time-spinner__wrapper:last-child {
  471. display: none;
  472. }
  473. .head {
  474. display: flex;
  475. font-weight: 500;
  476. }
  477. .head .head-item {
  478. flex: 1;
  479. text-align: center;
  480. font-size: 0.8em;
  481. border-top: 3px solid #fff;
  482. padding-top: 10px;
  483. }
  484. .head .value {
  485. margin: 15px 0;
  486. color: #396fff;
  487. font-size: 25px;
  488. }
  489. </style>