liyongli 2 年之前
父節點
當前提交
c05e107bfa
共有 4 個文件被更改,包括 1884 次插入30 次删除
  1. 86 30
      src/router/index.js
  2. 494 0
      src/views/Keep/index.vue
  3. 589 0
      src/views/Search/index.vue
  4. 715 0
      src/views/Version/index.vue

+ 86 - 30
src/router/index.js

@@ -4,95 +4,151 @@ const routes = [
   {
     path: "/",
     name: "Program",
-    component: () => import(/* webpackChunkName: "program" */ "../views/Program.vue"),
+    component: () =>
+      import(/* webpackChunkName: "program" */ "../views/Program.vue"),
   },
   {
     path: "/Channel",
     name: "Channel",
-    component: () => import(/* webpackChunkName: "channel" */ "../views/Channel.vue"),
+    component: () =>
+      import(/* webpackChunkName: "channel" */ "../views/Channel.vue"),
   },
   {
     path: "/heightlight",
     name: "Heightlight",
-    component: () => import(/* webpackChunkName: "heightlight" */ "../views/Heightlight.vue"),
+    component: () =>
+      import(/* webpackChunkName: "heightlight" */ "../views/Heightlight.vue"),
   },
   {
     path: "/realData",
     name: "RealData",
-    component: () => import(/* webpackChunkName: "realData" */ "../views/RealData.vue"),
+    component: () =>
+      import(/* webpackChunkName: "realData" */ "../views/RealData.vue"),
   },
   {
     path: "/liveChannel",
     name: "LiveChannel",
-    component: () => import(/* webpackChunkName: "liveChannel" */ "../views/LiveChannel.vue"),
+    component: () =>
+      import(/* webpackChunkName: "liveChannel" */ "../views/LiveChannel.vue"),
   },
   {
     path: "/boutiqueColumn",
     name: "boutiqueColumn",
-    component: () => import(/* webpackChunkName: "boutiqueColumn" */ "../views/BoutiqueColumn/BoutiqueColumn.vue"),
+    component: () =>
+      import(
+        /* webpackChunkName: "boutiqueColumn" */ "../views/BoutiqueColumn/BoutiqueColumn.vue"
+      ),
   },
   {
     path: "/activity",
     name: "Activity",
-    component: () => import(/* webpackChunkName: "activity" */ "../views/Activity/Activity.vue"),
+    component: () =>
+      import(
+        /* webpackChunkName: "activity" */ "../views/Activity/Activity.vue"
+      ),
   },
   {
     path: "/jugou",
     name: "Jugou",
-    component: () => import(/* webpackChunkName: "jugou" */ "../views/Jugou/Jugou.vue"),
+    component: () =>
+      import(/* webpackChunkName: "jugou" */ "../views/Jugou/Jugou.vue"),
   },
   {
     path: "/radio",
     name: "Radio",
-    component: () => import(/* webpackChunkName: "radio" */ "../views/Radio/Radio.vue"),
+    component: () =>
+      import(/* webpackChunkName: "radio" */ "../views/Radio/Radio.vue"),
   },
   {
-      path: "/hardAdvertisement",
-      name: "HardAdvertisement",
-      component: () => import(/* webpackChunkName: "hardAdvertisement" */ "../views/HardAdvertisement/HardAdvertisement.vue"),
+    path: "/hardAdvertisement",
+    name: "HardAdvertisement",
+    component: () =>
+      import(
+        /* webpackChunkName: "hardAdvertisement" */ "../views/HardAdvertisement/HardAdvertisement.vue"
+      ),
   },
   {
-      path: "/advertisingEye",
-      name: "AdvertisingEye",
-      component: () => import(/* webpackChunkName: "advertisingEye" */ "../views/AdvertisingEye/AdvertisingEye.vue"),
+    path: "/advertisingEye",
+    name: "AdvertisingEye",
+    component: () =>
+      import(
+        /* webpackChunkName: "advertisingEye" */ "../views/AdvertisingEye/AdvertisingEye.vue"
+      ),
   },
   {
-      path: "/channelRanking",
-      name: "ChannelRanking",
-      component: () => import(/* webpackChunkName: "channelRanking" */ "../views/ChannelRanking/ChannelRanking.vue"),
+    path: "/channelRanking",
+    name: "ChannelRanking",
+    component: () =>
+      import(
+        /* webpackChunkName: "channelRanking" */ "../views/ChannelRanking/ChannelRanking.vue"
+      ),
   },
   {
-      path: "/reportForm",
-      name: "ReportForm",
-      component: () => import(/* webpackChunkName: "reportForm" */ "../views/ReportForm/ReportForm.vue"),
+    path: "/reportForm",
+    name: "ReportForm",
+    component: () =>
+      import(
+        /* webpackChunkName: "reportForm" */ "../views/ReportForm/ReportForm.vue"
+      ),
   },
   {
-      path: "/radioBroadcast",
-      name: "RadioBroadcast",
-      component: () => import(/* webpackChunkName: "radioBroadcast" */ "../views/RadioBroadcast.vue")
+    path: "/radioBroadcast",
+    name: "RadioBroadcast",
+    component: () =>
+      import(
+        /* webpackChunkName: "radioBroadcast" */ "../views/RadioBroadcast.vue"
+      ),
   },
   {
     path: "/realOnline",
     name: "RealOnline",
-    component: () => import(/* webpackChunkName: "realOnline" */ "../views/RealOnline/index.vue")
+    component: () =>
+      import(
+        /* webpackChunkName: "realOnline" */ "../views/RealOnline/index.vue"
+      ),
   },
   {
     path: "/history",
     name: "History",
-    component: () => import(/* webpackChunkName: "history" */ "../views/History/index.vue")
+    component: () =>
+      import(/* webpackChunkName: "history" */ "../views/History/index.vue"),
   },
   {
     path: "/region",
     name: "Region",
-    component: () => import(/* webpackChunkName: "region" */ "../views/Region/index.vue")
+    component: () =>
+      import(/* webpackChunkName: "region" */ "../views/Region/index.vue"),
   },
+  //   内容
   {
     path: "/content",
     name: "Content",
-    component: () => import(/* webpackChunkName: "content" */ "../views/Content/index.vue")
+    component: () =>
+      import(/* webpackChunkName: "content" */ "../views/Content/index.vue"),
   },
-  
-  countryRouter // 全国数据
+  //   新用户留存
+  {
+    path: "/keep",
+    name: "Keep",
+    component: () =>
+      import(/* webpackChunkName: "Keep" */ "../views/Keep/index.vue"),
+  },
+  //   搜索分析
+  {
+    path: "/search",
+    name: "Search",
+    component: () =>
+      import(/* webpackChunkName: "Search" */ "../views/Search/index.vue"),
+  },
+  //   版本分析
+  {
+    path: "/version",
+    name: "Version",
+    component: () =>
+      import(/* webpackChunkName: "Search" */ "../views/Version/index.vue"),
+  },
+
+  countryRouter, // 全国数据
 ];
 // 实时/单期 节目流入流出 共计2个  可以使用同一模块
 // 具体/抽象 节目剧目 查询/排行  共计4个,可以使用一个模块

+ 494 - 0
src/views/Keep/index.vue

@@ -0,0 +1,494 @@
+<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="结束日期"
+            :clearable="false"
+          >
+          </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">
+      <el-table
+        v-if="this.oriData && this.oriData.list && this.oriData.list.length"
+        :data="showList"
+        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="startTimes" label="启动次数">
+          <template #default="scope">
+            <countTo
+              :startVal="scope.row.startTimes"
+              :endVal="scope.row.startTimes"
+              :duration="100"
+            ></countTo>
+          </template>
+        </el-table-column>
+        <el-table-column prop="duration" label="人均使用时长">
+          <template #default="scope">
+            {{ timeFormat(scope.row.durationUser) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="duration" label="次均使用时长">
+          <template #default="scope">
+            {{ timeFormat(scope.row.durationTimes) }}
+          </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 config from "@/config/index";
+export default {
+  name: "RealOnline",
+  data() {
+    return {
+      type: "",
+      lastParams: {},
+      page: 1,
+      form: {
+        app: "",
+        version: [],
+        channel: [],
+        date: [],
+      },
+      cycle: [],
+      oriData: {},
+      showList: [],
+      version: [],
+      channel: [],
+    };
+  },
+  async mounted() {
+    const { source, appV, appC, appli, appCLi, appVLi } =
+      await this.getAppListFunc();
+    const keys = {
+      value: "mname",
+      label: "mname",
+    };
+    this.cycle = this.verifyList(appli, source, keys, false);
+    this.channel = this.verifyList(appCLi, appC, keys, true);
+    this.version = this.verifyList(appVLi, appV, keys, true);
+    this.form = {
+      //   app: (this.cycle[0] || { value: "" }).value,
+      app: "起点新闻",
+      version: [(this.version[0] || { value: "" }).value],
+      channel: [(this.channel[0] || { value: "" }).value],
+      date: [new Date(Date.now() - 604800000), new Date(Date.now() - 86400000)],
+    };
+    this.onSubmit();
+  },
+  computed: {},
+  methods: {
+    verifyList(list, verify, obj, more) {
+      if (!obj) return;
+      let li = list || [];
+      const out = [];
+      more && out.push({ value: -1, label: "不限" });
+      for (let i = 0; i < li.length; i++) {
+        const v = li[i];
+        if (verify.length !== 0 && !verify[v.mcode]) continue;
+        out.push({
+          value: v[obj.value],
+          label: v[obj.label],
+        });
+      }
+      return out;
+    },
+    pagechange(p) {
+      this.page = p;
+      this.pushShowList();
+    },
+    pushShowList() {
+      let s = this.page - 1 < 0 ? 0 : (this.page - 1) * 10;
+      let e = this.page * 10;
+      let li = JSON.parse(JSON.stringify(this.oriData.list || []));
+      let out = [];
+      for (let i = s; i < e; i++) {
+        li[i] && out.push(li[i]);
+      }
+      this.showList = out;
+    },
+    onSubmit() {
+      this.lastParams = {
+        app: this.form.app,
+        start: this.FormData(this.form.date[0]),
+        end: this.FormData(this.form.date[1]),
+        manufacturer: this.form.channel == -1 ? undefined : this.form.channel,
+        version: this.form.version == -1 ? undefined : this.form.version,
+      };
+      getHistory(this.lastParams)
+        .then(r => {
+          let oriData = r || {};
+          this.oriData = {
+            list: oriData.list || [],
+          };
+          this.page = 1;
+          this.pushShowList();
+        })
+        .catch(() => {
+          this.oriData = {};
+          this.pushShowList();
+        });
+    },
+    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 clentV = { length: 0 },
+        clentli = [];
+      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,
+        clentV,
+        appVLi,
+        appCLi,
+        clentli,
+      };
+    },
+    async getAppListOri() {
+      const roleid = JSON.parse(
+        window.parent.localStorage.userinfo || "{}"
+      ).roleid;
+      const r = await getRule({
+        db: "authplat",
+        exportMark: "0",
+        menuid: 399,
+        roleid,
+      });
+      //   应用列表
+
+      const li = await getAppList({
+        exportMark: "0",
+        gcode: "SOURCE",
+        pageid: 1,
+        pagesize: 1000,
+      });
+      const defaultAppName = "起点新闻";
+      //   应用版本列表
+      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;
+      let out = [];
+      if (hour * 1 > 0) out.push(hour);
+      out.push(...[min, son]);
+      return out.join(":");
+    },
+    change() {
+      const roleid = JSON.parse(
+        window.parent.localStorage.userinfo || "{}"
+      ).roleid;
+      const appV = {
+        length: 0,
+      };
+      getRule({
+        db: "authplat",
+        exportMark: "0",
+        menuid: 399,
+        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,
+            this.timeFormat(v.durationUser),
+            this.timeFormat(v.durationTimes),
+            "\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>

+ 589 - 0
src/views/Search/index.vue

@@ -0,0 +1,589 @@
+<template>
+  <div class="Content">
+    <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="结束日期"
+            :clearable="false"
+          >
+          </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
+            collapse-tags
+            clearable
+            v-model="form.content"
+            placeholder="请选择内容"
+          >
+            <el-option
+              v-for="item in content"
+              :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-form-item>
+      </el-form>
+    </el-card>
+    <br />
+    <el-card class="box-card">
+      <el-row :gutter="20">
+        <el-col :span="6" v-for="(v, i) in top" :key="i + v.name">
+          <div class="libTitle" v-text="v.name"></div>
+          <el-table
+            :data="v.list || []"
+            style="width: 100%"
+            :header-cell-style="{
+              backgroundColor: '#f4f5f7',
+              color: '#606266',
+            }"
+          >
+            <el-table-column
+              prop="title"
+              header-align="center"
+              align="center"
+              label="标题"
+              show-overflow-tooltip
+            />
+            <el-table-column label="次数" header-align="center" align="center">
+              <template #default="scope">
+                <countTo
+                  :startVal="0"
+                  :endVal="scope.row[v.target] || 0"
+                  :duration="100"
+                ></countTo>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-col>
+      </el-row>
+      <br />
+      <el-row>
+        <el-col :span="8">
+        </el-col>
+        <el-col :span="16">
+          <el-button
+            size="small"
+            style="float: right"
+            type="primary"
+            @click="onExport"
+          >
+            导出
+          </el-button>
+        </el-col>
+      </el-row>
+      <br />
+      <br />
+      <el-table
+        :data="table.records || []"
+        style="width: 100%"
+        @sort-change="tableChange"
+        :header-cell-style="{
+          backgroundColor: '#f4f5f7',
+          color: '#606266',
+        }"
+      >
+        <el-table-column
+          prop="contentId"
+          header-align="center"
+          align="center"
+          label="内容ID"
+          show-overflow-tooltip
+        >
+          <template #default="scope">
+            <span v-text="scope.row.contentId || '-'"></span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="title"
+          header-align="center"
+          align="center"
+          width="300"
+          label="标题"
+          show-overflow-tooltip
+        >
+          <template #default="scope">
+            <span v-text="scope.row.title || '-'"></span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          show-overflow-tooltip
+          label="应用名称"
+          header-align="center"
+          align="center"
+        >
+          <template #default>
+            <span>{{ form.app || "-" }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="publishTime"
+          header-align="center"
+          align="center"
+          label="发布时间"
+          show-overflow-tooltip
+        >
+          <template #default="scope">
+            <span v-text="scope.row.publishTime || '-'"></span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="平均阅读时长"
+          header-align="center"
+          align="center"
+          sortable
+          :sort-orders="['descending', null]"
+          prop="cduration"
+        >
+          <template #default="scope">
+            <span v-text="timeFormat(scope.row.cduration)"></span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="阅读次数"
+          header-align="center"
+          align="center"
+          sortable
+          :sort-orders="['descending', null]"
+          prop="cpv"
+        >
+          <template #default="scope">
+            <countTo
+              :startVal="0"
+              :endVal="scope.row.cpv || 0"
+              :duration="100"
+            ></countTo>
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="点赞次数"
+          header-align="center"
+          align="center"
+          sortable
+          :sort-orders="['descending', null]"
+          prop="csupport"
+        >
+          <template #default="scope">
+            <countTo
+              :startVal="0"
+              :endVal="scope.row.csupport || 0"
+              :duration="100"
+            ></countTo>
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="收藏次数"
+          header-align="center"
+          align="center"
+          sortable
+          :sort-orders="['descending', null]"
+          prop="ccollect"
+        >
+          <template #default="scope">
+            <countTo
+              :startVal="0"
+              :endVal="scope.row.ccollect || 0"
+              :duration="100"
+            ></countTo>
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="分享次数"
+          header-align="center"
+          align="center"
+          sortable
+          :sort-orders="['descending', null]"
+          prop="cshare"
+        >
+          <template #default="scope">
+            <countTo
+              :startVal="0"
+              :endVal="scope.row.cshare || 0"
+              :duration="100"
+            ></countTo>
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="观看用户数"
+          header-align="center"
+          align="center"
+          sortable
+          :sort-orders="['descending', null]"
+          prop="cuser"
+        >
+          <template #default="scope">
+            <countTo
+              :startVal="0"
+              :endVal="scope.row.cuser || 0"
+              :duration="100"
+            ></countTo>
+          </template>
+        </el-table-column>
+      </el-table>
+      <br />
+      <el-pagination
+        layout="prev, pager, next"
+        :page-size="form.size"
+        :current-page="form.page"
+        :total="table.total"
+        background
+        @current-change="changePage"
+      />
+    </el-card>
+  </div>
+</template>
+<script>
+// @ is an alias to /src
+import {
+  getRule,
+  getAppList,
+  getSearchData,
+  getContentTop10,
+  getContentPage,
+} from "@/api/index";
+
+import countTo from "@/components/counto/vue-countTo.vue";
+
+import config from "@/config/index";
+export default {
+  name: "Content",
+  data() {
+    return {
+      lastParams: {},
+      table: {},
+      form: {
+        app: "",
+        version: [],
+        date: [],
+        content: "",
+      },
+      cycle: [],
+      showList: [],
+      version: [],
+      content: [],
+      top: [],
+    };
+  },
+  async mounted() {
+    const { source, appV, contentV, appli, appVLi, contentli } =
+      await this.getAppListFunc();
+    const keys = {
+      value: "mname",
+      label: "mname",
+    };
+    this.cycle = this.verifyList(appli, source, keys, false);
+    this.version = this.verifyList(appVLi, appV, keys, true);
+    this.content = this.verifyList(contentli, contentV, keys, true);
+    this.form = {
+      //   app: (this.cycle[0] || { value: "" }).value,
+      app: "起点新闻",
+      version: [(this.version[0] || { value: "" }).value],
+      content: -1,
+      date: [new Date(Date.now() - 604800000), new Date(Date.now() - 86400000)],
+      page: 1,
+      size: 20,
+      sortBy: "",
+    };
+    this.onSubmit();
+  },
+  computed: {},
+  methods: {
+    tableChange(e) {
+      this.form.page = 1;
+      this.form.sortBy = e.column ? e.column.property : "";
+      this.lastParams.sortBy = e.column ? e.column.property : "";
+      this.lastParams.page = 1;
+      getContentPage(this.lastParams).then(r => (this.table = r || {}));
+    },
+    verifyList(list, verify, obj, more) {
+      if (!obj) return;
+      let li = list || [];
+      const out = [];
+      more && out.push({ value: -1, label: "不限" });
+      for (let i = 0; i < li.length; i++) {
+        const v = li[i];
+        if (verify.length !== 0 && !verify[v.mcode]) continue;
+        out.push({
+          value: v[obj.value],
+          label: v[obj.label],
+        });
+      }
+      return out;
+    },
+    changePage(e) {
+      this.form.page = e;
+      this.onSubmit();
+    },
+    onSubmit() {
+      this.lastParams = {
+        app: this.form.app,
+        start: this.FormData(this.form.date[0]),
+        end: this.FormData(this.form.date[1]),
+        version: this.form.version == -1 ? undefined : this.form.version,
+        contentType: this.form.content == -1 ? undefined : this.form.content,
+        page: this.form.page,
+        pageSize: this.form.size,
+        sortBy: this.form.sortBy,
+      };
+      getContentPage(this.lastParams).then(r => (this.table = r || {}));
+      getContentTop10({
+        app: this.form.app,
+        start: this.FormData(this.form.date[0]),
+        end: this.FormData(this.form.date[1]),
+        version: this.form.version == -1 ? undefined : this.form.version,
+        contentType: this.form.content == -1 ? undefined : this.form.content,
+      })
+        .then(r => {
+          this.top = r || [];
+        });
+    },
+    async getAppListFunc() {
+      const { r, li, appVersion, contentList } = await this.getAppListOri();
+      let source = { length: 0 },
+        appli = [];
+      let appV = { length: 0 },
+        appVLi = [];
+      let contentV = { length: 0 },
+        contentli = [];
+      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 == "content_TYPE")
+          (contentV[v.detid] = true), (contentV.length = contentV.length + 1);
+      }
+      if (li.status === "0") appli = li.output.data || [];
+      if (appVersion.length) appVLi = appVersion || [];
+      if (contentList.length) contentli = contentList || [];
+      return {
+        source,
+        appV,
+        appli,
+        contentV,
+        appVLi,
+        contentli,
+      };
+    },
+    async getAppListOri() {
+      const roleid = JSON.parse(
+        window.parent.localStorage.userinfo || "{}"
+      ).roleid;
+      const r = await getRule({
+        db: "authplat",
+        exportMark: "0",
+        menuid: 399,
+        roleid,
+      });
+      //   应用列表
+
+      const li = await getAppList({
+        exportMark: "0",
+        gcode: "SOURCE",
+        pageid: 1,
+        pagesize: 1000,
+      });
+      const defaultAppName = "起点新闻";
+      //   应用版本列表
+      const appVersion = await getSearchData({
+        gcode: "APP_VERSION",
+        source: defaultAppName,
+      });
+      // 端列表
+      let contentList = await getAppList({
+        exportMark: "0",
+        gcode: "CONTENT_TYPE",
+        pageid: 1,
+        pagesize: 1000,
+      });
+      contentList =
+        contentList.status == "0" ? contentList.output.data || [] : [];
+      return { r, li, appVersion, contentList };
+    },
+    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;
+      let out = [];
+      if (hour * 1 > 0) out.push(hour);
+      out.push(...[min, son]);
+      return out.join(":");
+    },
+    change() {
+      const roleid = JSON.parse(
+        window.parent.localStorage.userinfo || "{}"
+      ).roleid;
+      const appV = {
+        length: 0,
+      };
+      getRule({
+        db: "authplat",
+        exportMark: "0",
+        menuid: 399,
+        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;
+    },
+    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;
+      const app = this.lastParams.app || this.form.app;
+      const version =
+        this.form.version == -1 ? undefined : this.form.version.join(",");
+      const contentType = this.lastParams.contentType || this.form.contentType;
+      let url =
+        config.base.leverAudience + "/cxzx-program/new-media/content/export?start=" + S + "&end=" + E +"&app=" + app;
+      version && (url += "&version=" + version);
+      contentType && (url += "&contentType=" + contentType);
+      window.open(url);
+    },
+  },
+  components: {
+    countTo,
+  },
+};
+</script>
+
+<style>
+.Content {
+  margin: 10px 15px;
+}
+.libTitle {
+  color: tomato;
+  font-weight: 700;
+  font-size: 14px;
+}
+.btn-next i,
+.btn-prev i {
+  margin: 0 auto;
+}
+.caret-wrapper .ascending {
+  display: none;
+}
+</style>

+ 715 - 0
src/views/Version/index.vue

@@ -0,0 +1,715 @@
+<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="结束日期"
+            :clearable="false"
+          >
+          </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 label="来源">
+          <el-select
+            collapse-tags
+            clearable
+            v-model="form.client"
+            placeholder="请选择来源"
+          >
+            <el-option
+              v-for="item in client"
+              :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
+          v-for="(item, i) in oriData.total"
+          :key="i + item.key"
+          class="head-item"
+          @click="
+            () =>
+              item.key !== 'downloads' &&
+              type !== item.key &&
+              changeData(item.key, item.name)
+          "
+          :style="{
+            borderTopColor: type === item.key ? '#396fff' : '#fff',
+          }"
+        >
+          <div>{{ item.name }}</div>
+          <div class="value">
+            <!-- oriData.total.activeUser -->
+            <countTo
+              v-if="!item.isNum"
+              :startVal="0"
+              :endVal="item.value - 0"
+              :duration="1500"
+            ></countTo>
+            <span v-if="item.isNum" v-text="item.value"></span>
+          </div>
+        </div>
+      </div>
+
+      <div class="realLineChart" ref="realLineChart"></div>
+      <el-table
+        v-if="this.oriData && this.oriData.list && this.oriData.list.length"
+        :data="showList"
+        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="startTimes" label="启动次数">
+          <template #default="scope">
+            <countTo
+              :startVal="scope.row.startTimes"
+              :endVal="scope.row.startTimes"
+              :duration="100"
+            ></countTo>
+          </template>
+        </el-table-column>
+        <el-table-column prop="duration" label="人均使用时长">
+          <template #default="scope">
+            {{ timeFormat(scope.row.durationUser) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="duration" label="次均使用时长">
+          <template #default="scope">
+            {{ timeFormat(scope.row.durationTimes) }}
+          </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,
+  ToolboxComponent,
+  LegendComponent,
+} from "echarts/components";
+import { CanvasRenderer } from "echarts/renderers";
+echarts.use([
+  TitleComponent,
+  TooltipComponent,
+  GridComponent,
+  LineChart,
+  CanvasRenderer,
+  ToolboxComponent,
+  LegendComponent,
+]);
+
+// import config from "@/config/index";
+let chart = undefined;
+export default {
+  name: "RealOnline",
+  data() {
+    return {
+      type: "",
+      lastParams: {},
+      page: 1,
+      form: {
+        app: "",
+        version: [],
+        channel: [],
+        date: [],
+        client: ""
+      },
+      cycle: [],
+      oriData: {},
+      showList: [],
+      version: [],
+      channel: [],
+      client: []
+    };
+  },
+  async mounted() {
+    if (chart && chart.dispose) chart.dispose();
+    const { source, appV, appC, clentV, appli, appCLi, appVLi, clentli } =
+      await this.getAppListFunc();
+    const keys = {
+      value: "mname",
+      label: "mname",
+    };
+    let client = clentli.find(r=>r.mdefault).mcode.toString();
+    this.cycle = this.verifyList(appli, source, keys, false);
+    this.channel = this.verifyList(appCLi, appC, keys, true);
+    this.version = this.verifyList(appVLi, appV, keys, true);
+    this.client = this.verifyList(clentli, clentV, {
+      value: "mcode",
+      label: "mname",
+    }, true);
+    this.form = {
+      //   app: (this.cycle[0] || { value: "" }).value,
+      app: "起点新闻",
+      version: [(this.version[0] || { value: "" }).value],
+      client,
+      channel: [(this.channel[0] || { value: "" }).value],
+      date: [new Date(Date.now() - 604800000), new Date(Date.now() - 86400000)],
+    };
+    this.onSubmit();
+  },
+  computed: {},
+  methods: {
+    verifyList(list, verify, obj, more) {
+      if (!obj) return;
+      let li = list || [];
+      const out = [];
+      more && out.push({ value: -1, label: "不限" });
+      for (let i = 0; i < li.length; i++) {
+        const v = li[i];
+        if (verify.length !== 0 && !verify[v.mcode]) continue;
+        out.push({
+          value: v[obj.value],
+          label: v[obj.label],
+        });
+      }
+      return out;
+    },
+    pagechange(p) {
+      this.page = p;
+      this.pushShowList();
+    },
+    pushShowList() {
+      let s = this.page - 1 < 0 ? 0 : (this.page - 1) * 10;
+      let e = this.page * 10;
+      let li = JSON.parse(JSON.stringify(this.oriData.list || []));
+      let out = [];
+      for (let i = s; i < e; i++) {
+        li[i] && out.push(li[i]);
+      }
+      this.showList = out;
+    },
+    onSubmit() {
+      this.lastParams = {
+        app: this.form.app,
+        start: this.FormData(this.form.date[0]),
+        end: this.FormData(this.form.date[1]),
+        manufacturer: this.form.channel == -1 ? undefined : this.form.channel,
+        version: this.form.version == -1 ? undefined : this.form.version,
+        lib: this.form.client == -1 ? undefined : this.form.client,
+      };
+      getHistory(this.lastParams)
+        .then(r => {
+          if (!this.$refs.realLineChart) return;
+          let oriData = r || {};
+          const total = oriData.total || {},
+            keys = Object.keys(total),
+            color = ["rgb(244, 127, 146)", "rgb(17, 160, 248)"],
+            p = [];
+          let tab = undefined,
+            tabName = undefined;
+          for (let i = 0; i < keys.length; i++) {
+            const v = keys[i];
+            if (!total[v].value && total[v].value !== 0) continue;
+            let isNum = /duration/g.test(v);
+            let value = "";
+            if (isNum && typeof total[v].value === "number")
+              value = this.timeFormat(total[v].value);
+            else value = total[v].value;
+            if (!tab && v !== "downloads") {
+              tab = v;
+              tabName = total[v].name;
+            }
+            p.push({
+              name: total[v].name,
+              value,
+              color: color[i % 2],
+              key: v,
+              isNum,
+            });
+          }
+          this.oriData = {
+            list: oriData.list || [],
+            total: p,
+          };
+          this.changeData(tab, tabName);
+          chart.hideLoading();
+          this.page = 1;
+          this.pushShowList();
+        })
+        .catch(() => {
+          this.oriData = {};
+          this.pushShowList();
+          chart && chart.clear() && chart.hideLoading();
+        });
+    },
+    changeData(type, title) {
+      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, title);
+    },
+    createImage(keyList, valueList, title) {
+      !chart && (chart = echarts.init(this.$refs.realLineChart));
+      chart.resize({
+        height: (this.$refs.realLineChart.offsetWidth * 4) / 16,
+      });
+      const _this = this;
+      chart.setOption({
+        tooltip: {
+          trigger: "axis",
+          formatter(v) {
+            const item = v[0] || {};
+            let val = item.data || 0;
+            if (_this.type === "duration") val = _this.timeFormat(val);
+            return item.axisValue + "<br />" + title + ":" + val;
+          },
+        },
+        toolbox: {
+          feature: {
+            saveAsImage: {
+              type: "jpg",
+              name: "趋势",
+            },
+          },
+        },
+        legend: {
+          data: [title],
+        },
+        grid: {
+          left: "3%",
+          right: "4%",
+          bottom: "3%",
+          containLabel: true,
+        },
+        xAxis: {
+          type: "category",
+          boundaryGap: true,
+          data: keyList,
+        },
+        yAxis: {
+          type: "value",
+          scale: true,
+          minInterval: 1,
+          axisLabel: {
+            formatter(v) {
+              let val = v;
+              if (_this.type === "duration") val = _this.timeFormat(val);
+              else {
+                if (val >= 100000000) {
+                  val = (val / 100000000).toFixed(2) + "亿";
+                } else if (val >= 10000) {
+                  val = (val / 10000).toFixed(2) + "万";
+                }
+              }
+              return val;
+            },
+          },
+        },
+        series: [
+          {
+            name: title,
+            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, clientList } =
+        await this.getAppListOri();
+      let source = { length: 0 },
+        appli = [];
+      let appV = { length: 0 },
+        appVLi = [];
+      let appC = { length: 0 },
+        appCLi = [];
+      let clentV = { length: 0 },
+        clentli = [];
+      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 (v.controlid == "CLIENT_TYPE")
+          (clentV[v.detid] = true), (clentV.length = clentV.length + 1);
+      }
+      if (li.status === "0") appli = li.output.data || [];
+      if (appVersion.length) appVLi = appVersion || [];
+      if (channel.length) appCLi = channel || [];
+      if (clientList.length) clentli = clientList || [];
+      return {
+        source,
+        appV,
+        appC,
+        appli,
+        clentV,
+        appVLi,
+        appCLi,
+        clentli,
+      };
+    },
+    async getAppListOri() {
+      const roleid = JSON.parse(
+        window.parent.localStorage.userinfo || "{}"
+      ).roleid;
+      const r = await getRule({
+        db: "authplat",
+        exportMark: "0",
+        menuid: 399,
+        roleid,
+      });
+      //   应用列表
+
+      const li = await getAppList({
+        exportMark: "0",
+        gcode: "SOURCE",
+        pageid: 1,
+        pagesize: 1000,
+      });
+      const defaultAppName = "起点新闻";
+      //   应用版本列表
+      const appVersion = await getSearchData({
+        gcode: "APP_VERSION",
+        source: defaultAppName,
+      });
+      // 端列表
+      const clientList = await getSearchData({
+        gcode: "CLIENT_TYPE",
+        source: defaultAppName,
+      });
+      //   应用渠道列表
+      const channel = await getSearchData({
+        gcode: "CHANNEL",
+        source: defaultAppName,
+      });
+      return { r, li, appVersion, channel, clientList };
+    },
+    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;
+      let out = [];
+      if (hour * 1 > 0) out.push(hour);
+      out.push(...[min, son]);
+      return out.join(":");
+    },
+    change() {
+      const roleid = JSON.parse(
+        window.parent.localStorage.userinfo || "{}"
+      ).roleid;
+      const appV = {
+        length: 0,
+      };
+      getRule({
+        db: "authplat",
+        exportMark: "0",
+        menuid: 399,
+        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,
+            this.timeFormat(v.durationUser),
+            this.timeFormat(v.durationTimes),
+            "\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>