liyongli 2 年 前
コミット
5387d7a34f

+ 2 - 1
package.json

@@ -9,6 +9,7 @@
     "dll": "webpack --progress --config ./webpack.dll.js"
   },
   "dependencies": {
+    "-": "^0.0.1",
     "@element-plus/icons": "^0.0.11",
     "@element-plus/icons-vue": "^1.1.3",
     "axios": "^0.26.1",
@@ -21,7 +22,6 @@
     "vue-router": "^4.0.0-0"
   },
   "devDependencies": {
-    "typescript": "^4.3.5",
     "@vue/cli-plugin-babel": "~4.5.0",
     "@vue/cli-plugin-eslint": "~4.5.0",
     "@vue/cli-plugin-router": "~4.5.0",
@@ -32,6 +32,7 @@
     "compression-webpack-plugin": "5.0.1",
     "eslint": "^6.7.2",
     "eslint-plugin-vue": "^7.0.0",
+    "typescript": "^4.3.5",
     "unplugin-auto-import": "^0.6.6",
     "unplugin-element-plus": "^0.1.0",
     "unplugin-vue-components": "^0.15.4",

+ 6 - 0
pnpm-lock.yaml

@@ -1,6 +1,7 @@
 lockfileVersion: 5.3
 
 specifiers:
+  '-': ^0.0.1
   '@element-plus/icons': ^0.0.11
   '@element-plus/icons-vue': ^1.1.3
   '@vue/cli-plugin-babel': ~4.5.0
@@ -28,6 +29,7 @@ specifiers:
   webpack-cli: ^4.9.2
 
 dependencies:
+  '-': 0.0.1
   '@element-plus/icons': 0.0.11
   '@element-plus/icons-vue': 1.1.4_vue@3.2.37
   axios: 0.26.1
@@ -58,6 +60,10 @@ devDependencies:
 
 packages:
 
+  /-/0.0.1:
+    resolution: {integrity: sha512-3HfneK3DGAm05fpyj20sT3apkNcvPpCuccOThOPdzz8sY7GgQGe0l93XH9bt+YzibcTIgUAIMoyVJI740RtgyQ==}
+    dev: false
+
   /@achrinza/node-ipc/9.2.2:
     resolution: {integrity: sha512-b90U39dx0cU6emsOvy5hxU4ApNXnE3+Tuo8XQZfiKTGelDwpMwBVgBP7QX6dGTcJgu/miyJuNJ/2naFBliNWEw==}
     engines: {node: 8 || 10 || 12 || 14 || 16 || 17}

+ 26 - 0
src/api/index.js

@@ -849,3 +849,29 @@ export function getAppList(data) {
     data,
   });
 }
+
+
+/**
+ * 获取大数据平台app历史数据
+ * @return {AxjxPromise}
+ */
+ export function getHistory(data) {
+    return ajax({
+      urlType: "leverAudience",
+      url: "/cxzx-program/new-media/user/trend",
+      method: "POST",
+      data,
+    });
+  }
+  /**
+   * 获取大数据平台指定应用的版本号,渠道
+   * @return {AxjxPromise}
+   */
+   export function getSearchData(data) {
+      return ajax({
+        urlType: "leverAudience",
+        url: "/cxzx-program/new-media/param/info",
+        method: "POST",
+        data,
+      });
+    }

+ 5 - 0
src/components/counto/index.js

@@ -0,0 +1,5 @@
+import CountTo from './vue-countTo.vue';
+export default CountTo;
+if (typeof window !== 'undefined' && window.Vue) {
+  window.Vue.component('count-to', CountTo);
+}

+ 46 - 0
src/components/counto/requestAnimationFrame.js

@@ -0,0 +1,46 @@
+let lastTime = 0
+const prefixes = 'webkit moz ms o'.split(' ') // 各浏览器前缀
+
+let requestAnimationFrame
+let cancelAnimationFrame
+
+const isServer = typeof window === 'undefined'
+if (isServer) {
+  requestAnimationFrame = function() {
+    return
+  }
+  cancelAnimationFrame = function() {
+    return
+  }
+} else {
+  requestAnimationFrame = window.requestAnimationFrame
+  cancelAnimationFrame = window.cancelAnimationFrame
+  let prefix
+    // 通过遍历各浏览器前缀,来得到requestAnimationFrame和cancelAnimationFrame在当前浏览器的实现形式
+  for (let i = 0; i < prefixes.length; i++) {
+    if (requestAnimationFrame && cancelAnimationFrame) { break }
+    prefix = prefixes[i]
+    requestAnimationFrame = requestAnimationFrame || window[prefix + 'RequestAnimationFrame']
+    cancelAnimationFrame = cancelAnimationFrame || window[prefix + 'CancelAnimationFrame'] || window[prefix + 'CancelRequestAnimationFrame']
+  }
+
+  // 如果当前浏览器不支持requestAnimationFrame和cancelAnimationFrame,则会退到setTimeout
+  if (!requestAnimationFrame || !cancelAnimationFrame) {
+    requestAnimationFrame = function(callback) {
+      const currTime = new Date().getTime()
+      // 为了使setTimteout的尽可能的接近每秒60帧的效果
+      const timeToCall = Math.max(0, 16 - (currTime - lastTime))
+      const id = window.setTimeout(() => {
+        callback(currTime + timeToCall)
+      }, timeToCall)
+      lastTime = currTime + timeToCall
+      return id
+    }
+
+    cancelAnimationFrame = function(id) {
+      window.clearTimeout(id)
+    }
+  }
+}
+
+export { requestAnimationFrame, cancelAnimationFrame }

+ 191 - 0
src/components/counto/vue-countTo.vue

@@ -0,0 +1,191 @@
+<template>
+    <span>
+      {{displayValue}}
+    </span>
+</template>
+<script>
+import { requestAnimationFrame, cancelAnimationFrame } from './requestAnimationFrame.js'
+export default {
+  props: {
+    startVal: {
+      type: Number,
+      required: false,
+      default: 0
+    },
+    endVal: {
+      type: Number,
+      required: false,
+      default: 2017
+    },
+    duration: {
+      type: Number,
+      required: false,
+      default: 3000
+    },
+    autoplay: {
+      type: Boolean,
+      required: false,
+      default: true
+    },
+    decimals: {
+      type: Number,
+      required: false,
+      default: 0,
+      validator(value) {
+        return value >= 0
+      }
+    },
+    decimal: {
+      type: String,
+      required: false,
+      default: '.'
+    },
+    separator: {
+      type: String,
+      required: false,
+      default: ','
+    },
+    prefix: {
+      type: String,
+      required: false,
+      default: ''
+    },
+    suffix: {
+      type: String,
+      required: false,
+      default: ''
+    },
+    useEasing: {
+      type: Boolean,
+      required: false,
+      default: true
+    },
+    easingFn: {
+      type: Function,
+      default(t, b, c, d) {
+        return c * (-Math.pow(2, -10 * t / d) + 1) * 1024 / 1023 + b;
+      }
+    }
+  },
+  data() {
+    return {
+      localStartVal: this.startVal,
+      displayValue: this.formatNumber(this.startVal),
+      printVal: null,
+      paused: false,
+      localDuration: this.duration,
+      startTime: null,
+      timestamp: null,
+      remaining: null,
+      rAF: null
+    };
+  },
+  computed: {
+    countDown() {
+      return this.startVal > this.endVal
+    }
+  },
+  watch: {
+    startVal() {
+      if (this.autoplay) {
+        this.start();
+      }
+    },
+    endVal() {
+      if (this.autoplay) {
+        this.start();
+      }
+    }
+  },
+  mounted() {
+    if (this.autoplay) {
+      this.start();
+    }
+    this.$emit('mountedCallback')
+  },
+  methods: {
+    start() {
+      this.localStartVal = this.startVal;
+      this.startTime = null;
+      this.localDuration = this.duration;
+      this.paused = false;
+      this.rAF = requestAnimationFrame(this.count);
+    },
+    pauseResume() {
+      if (this.paused) {
+        this.resume();
+        this.paused = false;
+      } else {
+        this.pause();
+        this.paused = true;
+      }
+    },
+    pause() {
+      cancelAnimationFrame(this.rAF);
+    },
+    resume() {
+      this.startTime = null;
+      this.localDuration = +this.remaining;
+      this.localStartVal = +this.printVal;
+      requestAnimationFrame(this.count);
+    },
+    reset() {
+      this.startTime = null;
+      cancelAnimationFrame(this.rAF);
+      this.displayValue = this.formatNumber(this.startVal);
+    },
+    count(timestamp) {
+      if (!this.startTime) this.startTime = timestamp;
+      this.timestamp = timestamp;
+      const progress = timestamp - this.startTime;
+      this.remaining = this.localDuration - progress;
+
+      if (this.useEasing) {
+        if (this.countDown) {
+          this.printVal = this.localStartVal - this.easingFn(progress, 0, this.localStartVal - this.endVal, this.localDuration)
+        } else {
+          this.printVal = this.easingFn(progress, this.localStartVal, this.endVal - this.localStartVal, this.localDuration);
+        }
+      } else {
+        if (this.countDown) {
+          this.printVal = this.localStartVal - ((this.localStartVal - this.endVal) * (progress / this.localDuration));
+        } else {
+          this.printVal = this.localStartVal + (this.endVal - this.localStartVal) * (progress / this.localDuration);
+        }
+      }
+      if (this.countDown) {
+        this.printVal = this.printVal < this.endVal ? this.endVal : this.printVal;
+      } else {
+        this.printVal = this.printVal > this.endVal ? this.endVal : this.printVal;
+      }
+
+      this.displayValue = this.formatNumber(this.printVal)
+      if (progress < this.localDuration) {
+        this.rAF = requestAnimationFrame(this.count);
+      } else {
+        this.$emit('callback');
+      }
+    },
+    isNumber(val) {
+      return !isNaN(parseFloat(val))
+    },
+    formatNumber(num) {
+      num = num.toFixed(this.decimals);
+      num += '';
+      const x = num.split('.');
+      let x1 = x[0];
+      const x2 = x.length > 1 ? this.decimal + x[1] : '';
+      const rgx = /(\d+)(\d{3})/;
+      if (this.separator && !this.isNumber(this.separator)) {
+        while (rgx.test(x1)) {
+          x1 = x1.replace(rgx, '$1' + this.separator + '$2');
+        }
+      }
+      return this.prefix + x1 + x2 + this.suffix;
+    }
+  },
+  unmounted() {
+    cancelAnimationFrame(this.rAF)
+  }
+};
+</script>

+ 5 - 0
src/router/index.js

@@ -76,6 +76,11 @@ const routes = [
     name: "RealOnline",
     component: () => import("../views/RealOnline/index.vue")
   },
+  {
+    path: "/history",
+    name: "History",
+    component: () => import("../views/History/index.vue")
+  },
 
   {
     path: "/country",

+ 642 - 0
src/views/History/index.vue

@@ -0,0 +1,642 @@
+<template>
+  <div class="RealOnline">
+    <el-breadcrumb separator-class="el-icon-arrow-right">
+      <el-breadcrumb-item>新媒体</el-breadcrumb-item>
+      <el-breadcrumb-item>用户分析</el-breadcrumb-item>
+      <el-breadcrumb-item>用户趋势</el-breadcrumb-item>
+    </el-breadcrumb>
+    <el-card class="box-card">
+      <el-form
+        ref="form"
+        :model="form"
+        size="small"
+        :inline="true"
+        label-width="120px"
+        class="demo-form-inline"
+      >
+        <el-form-item label="日期">
+          <el-date-picker
+            v-if="form.date.length"
+            v-model="form.date"
+            type="daterange"
+            :disabled-date="time => disabledDate(time)"
+            range-separator="-"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+          >
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="应用">
+          <el-select
+            v-model="form.app"
+            placeholder="请选择时段"
+            @change="change"
+          >
+            <el-option
+              v-for="item in cycle"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+              :disabled="item.disabled"
+            >
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="版本">
+          <el-select
+            multiple
+            collapse-tags
+            clearable
+            v-model="form.version"
+            placeholder="请选择版本"
+            @change="changeversion"
+          >
+            <el-option
+              v-for="item in version"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+              :disabled="item.disabled"
+            >
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="渠道">
+          <el-select
+            multiple
+            collapse-tags
+            clearable
+            v-model="form.channel"
+            @change="changechannel"
+            placeholder="请选择渠道"
+          >
+            <el-option
+              v-for="item in channel"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+              :disabled="item.disabled"
+            >
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item style="float: right">
+          <el-button type="primary" @click="onSubmit">查询</el-button>
+          <el-button type="primary" @click="onExport">导出</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+    <br />
+    <el-card class="box-card">
+      <div class="head" v-if="oriData.total">
+        <div
+          class="head-item"
+          @click="() => changeData('activeUser')"
+          :style="{
+            borderTopColor: type === 'activeUser' ? '#396fff' : '#fff',
+          }"
+        >
+          <div>活跃用户</div>
+          <div class="value">
+            <!-- oriData.total.activeUser -->
+            <countTo
+              :startVal="0"
+              :endVal="oriData.total.activeUser"
+              :duration="1500"
+            ></countTo>
+          </div>
+        </div>
+        <div
+          class="head-item"
+          @click="() => changeData('newUser')"
+          :style="{ borderTopColor: type === 'newUser' ? '#396fff' : '#fff' }"
+        >
+          <div>新增用户</div>
+          <div class="value">
+            <countTo
+              :startVal="0"
+              :endVal="oriData.total.newUser"
+              :duration="1500"
+            ></countTo>
+          </div>
+        </div>
+        <div
+          class="head-item"
+          @click="() => changeData('totalUser')"
+          :style="{ borderTopColor: type === 'totalUser' ? '#396fff' : '#fff' }"
+        >
+          <div>累计用户</div>
+          <div class="value">
+            <countTo
+              :startVal="0"
+              :endVal="oriData.total.totalUser"
+              :duration="1500"
+            ></countTo>
+          </div>
+        </div>
+        <div
+          class="head-item"
+          @click="() => changeData('duration')"
+          :style="{ borderTopColor: type === 'duration' ? '#396fff' : '#fff' }"
+        >
+          <div>人均使用时长</div>
+          <div class="value">
+            {{ timeFormat(oriData.total.duration) }}
+          </div>
+        </div>
+      </div>
+
+      <div class="realLineChart" ref="realLineChart"></div>
+      <el-table
+        :data="table()"
+        style="width: 100%"
+        :header-cell-style="{ backgroundColor: '#f4f5f7', color: '#606266' }"
+      >
+        <el-table-column prop="dt" label="日期" />
+        <el-table-column prop="activeUser" label="活跃用户">
+          <template #default="scope">
+            <countTo
+              :startVal="scope.row.activeUser"
+              :endVal="scope.row.activeUser"
+              :duration="100"
+            ></countTo>
+          </template>
+        </el-table-column>
+        <el-table-column prop="newUser" label="新增用户">
+          <template #default="scope">
+            <countTo
+              :startVal="scope.row.newUser"
+              :endVal="scope.row.newUser"
+              :duration="100"
+            ></countTo>
+          </template>
+        </el-table-column>
+        <el-table-column prop="totalUser" label="累计用户">
+          <template #default="scope">
+            <countTo
+              :startVal="scope.row.totalUser"
+              :endVal="scope.row.totalUser"
+              :duration="100"
+            ></countTo>
+          </template>
+        </el-table-column>
+        <el-table-column prop="duration" label="人均使用时长">
+          <template #default="scope">
+            {{ timeFormat(scope.row.duration) }}
+          </template>
+        </el-table-column>
+      </el-table>
+      <div v-if="oriData.list && oriData.list.length">
+        <el-pagination
+          v-if="Math.ceil(oriData.list.length / 10) > 1"
+          :current-page="page"
+          layout="prev, pager, next"
+          :total="oriData.list.length"
+          @current-change="pagechange"
+        />
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+// @ is an alias to /src
+import { getRule, getAppList, getHistory, getSearchData } from "@/api/index";
+
+import countTo from "@/components/counto/vue-countTo.vue";
+
+import * as echarts from "echarts/core";
+import { LineChart } from "echarts/charts";
+import {
+  TitleComponent,
+  TooltipComponent,
+  GridComponent,
+} from "echarts/components";
+import { CanvasRenderer } from "echarts/renderers";
+echarts.use([
+  TitleComponent,
+  TooltipComponent,
+  GridComponent,
+  LineChart,
+  CanvasRenderer,
+]);
+
+// import config from "@/config/index";
+let chart = undefined,
+  time = undefined;
+export default {
+  name: "RealOnline",
+  data() {
+    return {
+      type: "activeUser",
+      lastParams: {},
+      page: 1,
+      form: {
+        app: "",
+        version: [],
+        channel: [],
+        date: [],
+      },
+      cycle: [],
+      oriData: {},
+      version: [
+        {
+          label: "不限",
+          value: -1,
+        },
+      ],
+      channel: [
+        {
+          label: "不限",
+          value: -1,
+        },
+      ],
+    };
+  },
+  mounted() {
+    this.init();
+  },
+  computed: {},
+  methods: {
+    table() {
+      let s = this.page - 1 < 0 ? 0 : (this.page - 1) * 10;
+      let e = this.page * 10;
+      let li = this.oriData.list || [];
+      let out = [];
+      for (let i = s; i < e; i++) {
+        li[i] && out.push(li[i]);
+      }
+      return out;
+    },
+    pagechange(p) {
+      this.page = p;
+    },
+    async init() {
+      if (chart && chart.dispose) chart.dispose();
+      const { source, appV, appC, appli, appCLi, appVLi } =
+        await this.getAppListFunc();
+      appli.map(v => {
+        if ((source.length && source[v.mcode]) || source.length === 0)
+          this.cycle.push({
+            value: v.mname,
+            label: v.mname,
+          });
+      });
+      appCLi.map(v => {
+        if ((appC.length && appC[v.mcode]) || appC.length === 0)
+          this.channel.push({
+            value: v.mname,
+            label: v.mname,
+          });
+      });
+      appVLi.map(v => {
+        if ((appV.length && appV[v.mcode]) || appV.length === 0)
+          this.version.push({
+            value: v.mname,
+            label: v.mname,
+          });
+      });
+      const T = new Date(Date.now() - 86400000);
+      this.form = {
+        app: (this.cycle[0] || { value: "" }).value,
+        version: [(this.version[0] || { value: "" }).value],
+        channel: [(this.channel[0] || { value: "" }).value],
+        date: [T, T],
+      };
+      this.regularData();
+    },
+    regularData() {
+      if (time) clearInterval(time);
+      chart &&
+        chart.showLoading({
+          text: "loading",
+          color: "rgba(58,132,255,.9)",
+          textColor: "#000",
+          maskColor: "rgba(255, 255, 255, 0.2)",
+          zlevel: 0,
+        });
+      this.onSubmit();
+    },
+    onSubmit() {
+      const S = this.form.date[0]
+        ? this.FormData(this.form.date[0])
+        : undefined;
+      const E = this.form.date[1]
+        ? this.FormData(this.form.date[1])
+        : undefined;
+      this.lastParams = {
+        app: this.form.app,
+        start: S,
+        end: E,
+        manufacturer: this.form.channel == -1 ? undefined : this.form.channel,
+        version: this.form.version == -1 ? undefined : this.form.version,
+      };
+      getHistory(this.lastParams)
+        .then(r => {
+          if (!this.$refs.realLineChart) return;
+          this.oriData = r || {};
+          this.changeData("activeUser");
+          chart.hideLoading();
+          this.page = 1;
+        })
+        .catch(() => {
+          chart && chart.clear() && chart.hideLoading();
+        });
+    },
+    changeData(type) {
+      this.type = type;
+      const keyList = [],
+        valueList = [];
+      (this.oriData.list || []).map(v => {
+        keyList.push(v.dt);
+        valueList.push(v[this.type]);
+      });
+      chart && chart.clear();
+      this.createImage(keyList, valueList);
+    },
+    createImage(keyList, valueList) {
+      !chart && (chart = echarts.init(this.$refs.realLineChart));
+      chart.resize({
+        height: (this.$refs.realLineChart.offsetWidth * 6) / 16,
+      });
+      chart.setOption({
+        tooltip: {
+          trigger: "axis",
+        },
+        grid: {
+          left: "3%",
+          right: "4%",
+          bottom: "3%",
+          containLabel: true,
+        },
+        xAxis: {
+          type: "category",
+          boundaryGap: false,
+          data: keyList,
+        },
+        yAxis: {
+          type: "value",
+          minInterval: 1,
+        },
+        series: [
+          {
+            data: valueList,
+            symbolSize: 0,
+            lineStyle: {
+              width: 1,
+            },
+            type: "line",
+            smooth: true,
+            color: "rgba(58,132,255,.9)",
+            areaStyle: {
+              color: {
+                type: "linear",
+                x: 0,
+                y: 0,
+                x2: 0,
+                y2: 1,
+                colorStops: [
+                  {
+                    offset: 0,
+                    color: "rgba(58,132,255, 0.8)", // 0% 处的颜色
+                  },
+                  {
+                    offset: 1,
+                    color: "rgba(58,132,255, 0.1)", // 100% 处的颜色
+                  },
+                ],
+                global: false, // 缺省为 false
+              },
+            },
+          },
+        ],
+      });
+    },
+    async getAppListFunc() {
+      const { r, li, appVersion, channel } = await this.getAppListOri();
+      let source = { length: 0 },
+        appli = [];
+      let appV = { length: 0 },
+        appVLi = [];
+      let appC = { length: 0 },
+        appCLi = [];
+      let prvList = r.output.data.prvRolectrl || [];
+      for (let i = 0; i < prvList.length; i++) {
+        const v = prvList[i];
+        if (v.controlid == "RMT_SOURCE")
+          (source[v.detid] = true), (source.length = source.length + 1);
+        if (v.controlid == "APP_VERSION")
+          (appV[v.detid] = true), (appV.length = appV.length + 1);
+        if (v.controlid == "CHANNEL")
+          (appC[v.detid] = true), (appC.length = appC.length + 1);
+      }
+      if (li.status === "0") appli = li.output.data || [];
+      if (appVersion.length) appVLi = appVersion || [];
+      if (channel.length) appCLi = channel || [];
+      return {
+        source,
+        appV,
+        appC,
+        appli,
+        appVLi,
+        appCLi,
+      };
+    },
+    async getAppListOri() {
+      const roleid = JSON.parse(
+        window.parent.localStorage.userinfo || "{}"
+      ).roleid;
+      const r = await getRule({
+        db: "authplat",
+        exportMark: "0",
+        menuid: 221,
+        roleid,
+      });
+      //   应用列表
+      const li = await getAppList({
+        exportMark: "0",
+        gcode: "SOURCE",
+        pageid: 1,
+        pagesize: 1000,
+      });
+      const defaultAppName = li.status == "0" ? li.output.data[0].mname : "";
+      //   应用版本列表
+      const appVersion = await getSearchData({
+        gcode: "APP_VERSION",
+        source: defaultAppName,
+      });
+      //   应用渠道列表
+      const channel = await getSearchData({
+        gcode: "CHANNEL",
+        source: defaultAppName,
+      });
+      return { r, li, appVersion, channel };
+    },
+    disabledDate(time) {
+      const first = new Date("2021-06-21 00:00:00");
+      return (
+        time.getTime() > Date.now() - 86400000 ||
+        time.getTime() < first.getTime()
+      );
+    },
+    FormData(date) {
+      const d = new Date(date || Date.now() - 86400000);
+      const year = d.getFullYear();
+      const month =
+        d.getMonth() < 8 ? "0" + (d.getMonth() + 1) : d.getMonth() + 1;
+      const day = d.getDate() < 9 ? "0" + d.getDate() : d.getDate();
+      return [year, month, day].join("-");
+    },
+    timeFormat(t) {
+      const Time = t || 0;
+      const mH = Time % 3600;
+      let hour = (Time - mH) / 3600;
+      let min = (mH - (mH % 60)) / 60;
+      let son = Number(mH % 60).toFixed(0);
+      hour = hour <= 9 ? "0" + hour : hour;
+      min = min <= 9 ? "0" + min : min;
+      son = son <= 9 ? "0" + son : son;
+      return [hour, min, son].join(":");
+    },
+    change() {
+      const roleid = JSON.parse(
+        window.parent.localStorage.userinfo || "{}"
+      ).roleid;
+      const appV = {
+        length: 0,
+      };
+      getRule({
+        db: "authplat",
+        exportMark: "0",
+        menuid: 221,
+        roleid,
+      }).then(rule => {
+        let prvList = rule.output.data.prvRolectrl || [];
+        for (let i = 0; i < prvList.length; i++) {
+          const v = prvList[i];
+          if (v.controlid == "APP_VERSION")
+            (appV[v.detid] = true), (appV.length = appV.length + 1);
+        }
+        getSearchData({
+          gcode: "APP_VERSION",
+          source: this.form.app,
+        }).then(r => {
+          let version = [
+            {
+              label: "不限",
+              value: -1,
+            },
+          ];
+          r.map(v => {
+            if ((appV.length && appV[v.mcode]) || appV.length === 0)
+              version.push({
+                value: v.mname,
+                label: v.mname,
+              });
+          });
+          this.version = version;
+          this.form = {
+            ...this.form,
+            version: [-1],
+          };
+        });
+      });
+    },
+    changeversion(v) {
+      if (!v.length) return (this.form.version = [-1]);
+      const last = v[v.length - 1];
+      if (last == -1) return (this.form.version = [-1]);
+      let ver = [];
+      for (let i = 0; i < v.length; i++) {
+        const element = v[i];
+        if (element == -1) continue;
+        ver.push(element);
+      }
+      this.form.version = ver;
+    },
+    changechannel(v) {
+      if (!v.length) return (this.form.channel = [-1]);
+      const last = v[v.length - 1];
+      if (last == -1) return (this.form.channel = [-1]);
+      let ver = [];
+      for (let i = 0; i < v.length; i++) {
+        const element = v[i];
+        if (element == -1) continue;
+        ver.push(element);
+      }
+      this.form.channel = ver;
+    },
+    onExport() {
+      const S = this.form.date[0]
+        ? this.FormData(this.form.date[0])
+        : undefined;
+      const E = this.form.date[1]
+        ? this.FormData(this.form.date[1])
+        : undefined;
+      let p = this.lastParams.app
+        ? this.lastParams
+        : {
+            app: this.form.app,
+            start: S,
+            end: E,
+            manufacturer:
+              this.form.channel == -1 ? undefined : this.form.channel,
+            version: this.form.version == -1 ? undefined : this.form.version,
+          };
+      getHistory(p).then(r => {
+        // 生成数据
+        let strcsv =
+          "data:text/csv;charset=utf-8,日期,活跃用户,新增用户,累计用户,人均使用时长\r\n";
+        (r.list || []).map(v => {
+          strcsv += [
+            v.dt,
+            v.activeUser,
+            v.newUser,
+            v.totalUser,
+            v.duration,
+            "\r\n",
+          ].join(",");
+        });
+        // 导出
+        let link = document.createElement("a");
+        link.id = "download-csv";
+        link.setAttribute("href", encodeURI(strcsv));
+        link.setAttribute(
+          "download",
+          p.app + "用户趋势" + S + ":" + E + ".csv"
+        );
+        document.body.appendChild(link);
+        link.click();
+      });
+    },
+  },
+  components: {
+    countTo,
+  },
+};
+</script>
+
+<style>
+.RealOnline {
+  margin: 10px 15px;
+}
+.RealOnline .has-seconds .el-time-spinner__wrapper:last-child {
+  display: none;
+}
+.head {
+  display: flex;
+  font-weight: 500;
+}
+.head .head-item {
+  flex: 1;
+  text-align: center;
+  font-size: 0.8em;
+  border-top: 3px solid #fff;
+  padding-top: 10px;
+}
+.head .value {
+  margin: 15px 0;
+  color: #396fff;
+  font-size: 25px;
+}
+</style>

+ 9 - 4
src/views/RealOnline/index.vue

@@ -85,7 +85,7 @@ export default {
       if (chart && chart.dispose) chart.dispose();
       const { source, appli } = await this.getAppListFunc();
       appli.map(v => {
-        if (source[v.mcode])
+        if ((length.length && source[v.mcode]) || source.length === 0)
           this.cycle.push({
             value: v.mname,
             label: v.mname,
@@ -136,7 +136,9 @@ export default {
           chart.hideLoading();
         })
         .catch(() => {
-          chart && chart.clear() && chart.hideLoading();
+          if (!chart) return;
+          chart.clear();
+          chart.hideLoading();
         });
     },
     createImage(keyList, valueList) {
@@ -199,12 +201,15 @@ export default {
     },
     async getAppListFunc() {
       const { r, li } = await this.getAppListOri();
-      let source = {},
+      let source = {
+          length: 0,
+        },
         appli = [];
       let prvList = r.output.data.prvRolectrl || [];
       for (let i = 0; i < prvList.length; i++) {
         const v = prvList[i];
-        if (v.controlid == "RMT_SOURCE") source[v.detid] = true;
+        if (v.controlid == "RMT_SOURCE")
+          (source[v.detid] = true), (source.length = source.length + 1);
       }
       if (li.status === "0") appli = li.output.data || [];
       return {