Ver código fonte

添加人物画像

liyongli 9 meses atrás
pai
commit
50b069a9bd
4 arquivos alterados com 889 adições e 0 exclusões
  1. 91 0
      src/api/portrait.js
  2. 2 0
      src/router/index.js
  3. 15 0
      src/router/portrait.js
  4. 781 0
      src/views/Portrait/Index.vue

+ 91 - 0
src/api/portrait.js

@@ -0,0 +1,91 @@
+import ajax from '@/utils/request.js';
+/**
+ * 获取应用
+ * @return {AxjxPromise}
+ */
+export function getAppList(data) {
+  return ajax({
+    urlType: 'url2',
+    url: '/portrait/appids',
+    method: 'GET',
+    data
+  });
+}
+
+/**
+ * 获取性别分布
+ * @return {AxjxPromise}
+ */
+export function getSexData(data) {
+  return ajax({
+    urlType: 'url2',
+    url: '/portrait/sex',
+    method: 'GET',
+    data
+  });
+}
+
+/**
+ * 获取年龄分布
+ * @return {AxjxPromise}
+ */
+export function getAgeData(data) {
+  return ajax({
+    urlType: 'url2',
+    url: '/portrait/age',
+    method: 'GET',
+    data
+  });
+}
+
+/**
+ * 获取城市分布
+ * @return {AxjxPromise}
+ */
+export function getCityData(data) {
+  return ajax({
+    urlType: 'url2',
+    url: '/portrait/city',
+    method: 'GET',
+    data
+  });
+}
+
+/**
+ * 获取兴趣分布
+ * @return {AxjxPromise}
+ */
+export function getTagData(data) {
+  return ajax({
+    urlType: 'url2',
+    url: '/portrait/tag',
+    method: 'GET',
+    data
+  });
+}
+
+/**
+ * 获取机型号分布
+ * @return {AxjxPromise}
+ */
+export function getPhoneData(data) {
+  return ajax({
+    urlType: 'url2',
+    url: '/portrait/manufacturer',
+    method: 'GET',
+    data
+  });
+}
+
+/**
+ * 获取app栏目分布
+ * @return {AxjxPromise}
+ */
+export function getCategoryData(data) {
+  return ajax({
+    urlType: 'url2',
+    url: '/portrait/category',
+    method: 'GET',
+    data
+  });
+}

+ 2 - 0
src/router/index.js

@@ -2,6 +2,7 @@ import { createRouter, createWebHashHistory } from "vue-router";
 import countryRouter from "./country";
 import countryRouter from "./country";
 import rankRouter from "./rank";
 import rankRouter from "./rank";
 import rankRouter_old from "./rank_old";
 import rankRouter_old from "./rank_old";
+import portrait from "./portrait";
 const routes = [
 const routes = [
   {
   {
     path: "/",
     path: "/",
@@ -246,6 +247,7 @@ const routes = [
   },
   },
 
 
   countryRouter, // 全国数据
   countryRouter, // 全国数据
+  portrait // 用户画像
 ];
 ];
 // 实时/单期 节目流入流出 共计2个  可以使用同一模块
 // 实时/单期 节目流入流出 共计2个  可以使用同一模块
 // 具体/抽象 节目剧目 查询/排行  共计4个,可以使用一个模块
 // 具体/抽象 节目剧目 查询/排行  共计4个,可以使用一个模块

+ 15 - 0
src/router/portrait.js

@@ -0,0 +1,15 @@
+// 用户画像
+export default {
+    path: "/portrait",
+    // component: () =>
+    //   import(/* webpackChunkName: "portrait" */ "../views/Portrait/Index.vue"),
+    children: [
+      {
+        path: "",
+        component: () =>
+          import(
+            /* webpackChunkName: "portrait" */ "../views/Portrait/Index.vue"
+          ),
+      }
+    ],
+  }

+ 781 - 0
src/views/Portrait/Index.vue

@@ -0,0 +1,781 @@
+<template>
+  <el-container class="portrait">
+    <el-header style="height: auto; padding-top: 1em">
+      <el-card>
+        <el-form ref="form" size="small" :inline="true" label-width="120px">
+          <el-form-item label="选择应用">
+            <el-select v-model="appSelect" placeholder="请选择">
+              <el-option
+                v-for="item in appList"
+                :key="item.appid"
+                :label="item.name"
+                :value="item.appid"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item style="float: right">
+            <el-button type="primary" @click="getData"> 查询 </el-button>
+          </el-form-item>
+        </el-form>
+      </el-card>
+    </el-header>
+    <el-main>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-card>
+            <template #header>
+              <span>用户性别分布</span>
+            </template>
+            <div ref="sex"></div>
+          </el-card>
+        </el-col>
+        <el-col :span="12">
+          <el-card>
+            <template #header>
+              <span>用户年龄分布</span>
+            </template>
+            <div ref="age"></div>
+          </el-card>
+        </el-col>
+      </el-row>
+      <br />
+      <el-card>
+        <template #header>
+          <span>用户标签分布</span>
+        </template>
+        <div ref="tag"></div>
+      </el-card>
+      <br />
+      <el-card>
+        <template #header>
+          <span>陕西省内用户分布</span>
+        </template>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <div ref="city"></div>
+          </el-col>
+          <el-col :span="12">
+            <el-table :data="tableProData" style="width: 100%">
+              <el-table-column prop="category" label="地名" />
+              <el-table-column center prop="ct" label="访问人数" />
+            </el-table>
+          </el-col>
+        </el-row>
+      </el-card>
+      <br />
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-card>
+            <template #header>
+              <span>用户机型分布</span>
+            </template>
+            <div ref="phone"></div>
+          </el-card>
+        </el-col>
+        <el-col :span="12">
+          <el-card>
+            <template #header>
+              <span>APP栏目分布</span>
+            </template>
+            <div ref="category"></div>
+          </el-card>
+        </el-col>
+      </el-row>
+    </el-main>
+  </el-container>
+</template>
+
+<script setup>
+import {
+  getAppList,
+  getSexData,
+  getAgeData,
+  getCityData,
+  getTagData,
+  getPhoneData,
+  getCategoryData
+} from '@/api/portrait.js';
+import { ref } from 'vue';
+
+import shanxi from '../../assets/map/610000_shaanxi.json';
+import * as echarts from 'echarts/core';
+import {
+  PieChart,
+  BarChart,
+  PictorialBarChart,
+  MapChart
+} from 'echarts/charts';
+import {
+  TooltipComponent,
+  LegendComponent,
+  GridComponent,
+  TitleComponent,
+  VisualMapComponent
+} from 'echarts/components';
+import { CanvasRenderer } from 'echarts/renderers';
+echarts.use([
+  PieChart,
+  MapChart,
+  BarChart,
+  PictorialBarChart,
+  TooltipComponent,
+  CanvasRenderer,
+  LegendComponent,
+  GridComponent,
+  TitleComponent,
+  VisualMapComponent
+]);
+// 陕西地图引入
+echarts.registerMap('ShanXi', shanxi);
+
+const appList = ref([]);
+const tableProData = ref([]);
+const appSelect = ref('');
+const sex = ref(null);
+const age = ref(null);
+const tag = ref(null);
+const city = ref(null);
+const phone = ref(null);
+const category = ref(null);
+
+getAppList().then(res => {
+  appSelect.value = (res || [])[0].appid;
+  appList.value = res || [];
+  getData();
+});
+
+const numform = n => {
+  let num = n;
+  if (isNaN(n)) num = '0';
+  else if (n >= 100000000)
+    num =
+      ((n / 100000000).toFixed(2) - 0 + '').replace(
+        /\B(?=(?:\d{3})+\b)/g,
+        ','
+      ) + '亿';
+  else if (n >= 10000)
+    num =
+      ((n / 10000).toFixed(2) - 0 + '').replace(/\B(?=(?:\d{3})+\b)/g, ',') +
+      '万';
+  else num = (num + '').replace(/\B(?=(?:\d{3})+\b)/g, ',');
+  return num;
+};
+
+let sexChart = undefined;
+const createSex = (list = []) => {
+  if (!list || list.length == 0) return;
+  sexChart = echarts.init(sex.value);
+  sexChart.resize({
+    height: sex.value.offsetWidth
+  });
+  const keys = list.map(v => v.category);
+  sexChart.setOption({
+    tooltip: {
+      trigger: 'item',
+      formatter: '{b} : {d}%'
+    },
+    legend: {
+      // 图例-图上面的分类
+      orient: 'vertical',
+      right: 30,
+      //   icon: 'rect',//长方形
+      icon: 'circle',
+      bottom: '20%',
+      itemWidth: 10,
+      itemHeight: 10,
+      itemGap: 13,
+      data: keys,
+      // right: '56%',
+      textStyle: {
+        fontSize: 14,
+        color: '#a6cde8',
+        lineHeight: 49
+      },
+      formatter: function (name) {
+        return '' + name + '';
+      },
+      padding: [2, 2]
+    },
+    grid: {
+      top: '20%',
+      left: '53%',
+      right: '10%',
+      bottom: '6%',
+      containLabel: true
+    },
+    series: [
+      {
+        label: {
+          normal: {
+            formatter: '{b}\n{d}%'
+          }
+        },
+        type: 'pie',
+        radius: '50%',
+        center: ['50%', '50%'],
+        data: list.map(v => {
+          return {
+            name: v.category,
+            value: v.ct
+          };
+        })
+      }
+    ]
+  });
+};
+
+let ageChart = undefined;
+const createAge = (list = []) => {
+  if (!list || list.length == 0) return;
+  ageChart = echarts.init(age.value);
+  ageChart.resize({
+    height: age.value.offsetWidth
+  });
+  const keys = [],
+    values = [];
+  for (let i = 0; i < list.length; i++) {
+    const v = list[i];
+    keys.push(v.category);
+    values.push(v.ct);
+  }
+  ageChart.setOption({
+    title: {},
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      }
+    },
+    legend: {
+      itemWidth: 10,
+      itemHeight: 10,
+      itemGap: 30,
+      left: '15%',
+      top: '3%',
+      textStyle: {
+        color: '#9B9B9B'
+      }
+    },
+    grid: {
+      left: '10%',
+      right: '10%',
+      top: '10%',
+      bottom: '10%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'value',
+      boundaryGap: [0, 0.01],
+      splitLine: { show: true },
+      axisLabel: {
+        textStyle: {
+          color: '#9B9B9B'
+        }
+      }
+    },
+    yAxis: {
+      type: 'category',
+      data: keys,
+      splitLine: { show: false },
+      axisLabel: {
+        textStyle: {
+          color: '#9B9B9B'
+        }
+      },
+      axisTick: { show: false }
+    },
+    series: [
+      {
+        type: 'bar',
+        data: values,
+        itemStyle: {
+          color: 'rgba(255,77,79,0.35)',
+          borderColor: '#FF4D4F'
+        },
+        label: {
+          formatter: data => {
+            return numform(data.data);
+          },
+          show: true,
+          position: 'right',
+          textStyle: {
+            fontFamily: 'Noto Sans SC',
+            fontSize: 14,
+            color: '#000'
+          }
+        }
+      }
+    ]
+  });
+};
+
+let tagChart = undefined;
+const createTag = (list = []) => {
+  if (!list || list.length == 0) return;
+  tagChart = echarts.init(tag.value);
+  tagChart.resize({
+    height: (tag.value.offsetWidth / 16) * 9
+  });
+  let category = list.map(v => {
+    return {
+      name: v.category,
+      value: v.ct
+    };
+  });
+  let yName = []; // y轴名称
+  let bgData = []; // 最大值用作背景显示的数据
+  let maxValue = category[0].value; //最大值
+  category = category.reverse();
+  category.forEach(element => {
+    yName.push(element.name);
+    bgData.push({
+      name: element.name,
+      value: maxValue,
+      type: element.type
+    });
+  });
+  tagChart.setOption({
+    xAxis: {
+      max: maxValue,
+      splitLine: {
+        show: false
+      },
+      axisLine: {
+        show: false
+      },
+      axisLabel: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      }
+    },
+    grid: {
+      left: 50,
+      top: 20,
+      right: 80,
+      bottom: 0
+    },
+    yAxis: [
+      {
+        // 每条图形上面的文字
+        inverse: false,
+        data: yName,
+        axisLabel: {
+          padding: [0, 0, 45, 0],
+          inside: true,
+          textStyle: {
+            fontSize: 12,
+            fontWeight: 400,
+            color: '#B1C3DD',
+            align: 'left'
+          },
+          formatter: '{value}',
+          rich: {
+            a: {
+              color: 'transparent',
+              lineHeight: 20,
+              fontSize: 14,
+              shadowColor: 'rgba(255, 0, 0, 1)',
+              shadowBlur: 10
+            }
+          }
+        },
+        splitLine: {
+          show: false
+        },
+        axisTick: {
+          show: false
+        },
+        axisLine: {
+          show: false
+        }
+      },
+      {
+        // y轴最左侧的文字
+        axisTick: 'none',
+        axisLine: 'none',
+        position: 'left',
+        axisLabel: {
+          padding: [3, 12, 13, 10], // 调整文字位置
+          textStyle: {
+            color: '#000',
+            fontSize: '12',
+            fontWeight: '400'
+          }
+        },
+        data: Array.from({ length: yName.length }, (_, i) => i + 1).reverse()
+      },
+      {
+        // y轴最右侧的文字
+        axisTick: 'none',
+        axisLine: 'none',
+        type: 'category',
+        axisLabel: {
+          margin: 10,
+          textStyle: {
+            color: '#FFAC26',
+            fontSize: '24'
+          }
+        },
+        data: category
+      }
+    ],
+    series: [
+      {
+        // 背景样式
+        name: '背景',
+        type: 'bar',
+        barWidth: 18,
+        stack: '总量',
+        barGap: '-100%',
+        symbol: 'fixed',
+        symbolRepeat: 'repeat',
+        legendHoverLink: false,
+        itemStyle: {
+          normal: {
+            color: 'rgba(153, 153, 153, 0)'
+          }
+        },
+        data: bgData,
+        animation: false, //关闭动画
+        z: 0
+      },
+      {
+        name: 'info',
+        // 内(显示的内容)
+        type: 'bar',
+        barGap: '-100%',
+        barWidth: 18,
+        legendHoverLink: false,
+        silent: true,
+        itemStyle: {
+          normal: {
+            color: function (params) {
+              var color;
+              if (params.dataIndex % 2 != 0) {
+                color = {
+                  type: 'linear',
+                  x: 0,
+                  y: 0,
+                  x2: 1,
+                  y2: 0,
+                  colorStops: [
+                    {
+                      offset: 0,
+                      color: 'rgba(46,85,185,0)' // 0% 处的颜色
+                    },
+                    {
+                      offset: 1,
+                      color: '#317fff' // 100% 处的颜色
+                    }
+                  ]
+                };
+              } else {
+                color = {
+                  type: 'linear',
+                  x: 0,
+                  y: 0,
+                  x2: 1,
+                  y2: 0,
+                  colorStops: [
+                    {
+                      offset: 0,
+                      color: 'rgba(46,85,185,0)' // 0% 处的颜色
+                    },
+                    {
+                      offset: 1,
+                      color: '#317fff' // 100% 处的颜色
+                    }
+                  ]
+                };
+              }
+              return color;
+            }
+          }
+        },
+        data: category,
+        z: 1,
+        animationEasing: 'elasticOut'
+      },
+      {
+        // 分隔
+        type: 'pictorialBar',
+        itemStyle: {
+          normal: {
+            color: 'rgba(255,255,255,1)'
+          }
+        },
+        symbolRepeat: 'fixed',
+        barWidth: 18,
+        symbolMargin: 4,
+        symbol: 'rect',
+        symbolClip: true,
+        symbolSize: [2, 18],
+        symbolPosition: 'start',
+        symbolOffset: [0, 0],
+        symbolBoundingData: maxValue,
+        data: bgData,
+        animation: false, //关闭动画
+        z: 2
+      }
+    ]
+  });
+};
+
+let cityChart = undefined;
+const createCity = (list = []) => {
+  if (!list || list.length == 0) return;
+  tableProData.value = list;
+  let max = -Infinity,
+    min = Infinity,
+    t = 0;
+  for (let i = 0; i < list.length; i++) {
+    const v = list[i];
+    if (v.ct > max) max = v.ct;
+    if (v.ct < min) min = v.ct;
+    t += v.ct;
+  }
+  cityChart = echarts.init(city.value);
+  cityChart.resize({
+    height: city.value.offsetWidth
+  });
+  cityChart.setOption({
+    tooltip: {
+      confine: true,
+      trigger: 'item',
+      formatter: function (e) {
+        if (isNaN(e.value)) return;
+        return (
+          e.name +
+          '<br />人数:' +
+          (e.value || '') +
+          '<br />占比:' +
+          e.data.distribution
+        );
+      }
+    },
+    visualMap: {
+      min,
+      max,
+      text: [max, min],
+      realtime: false,
+      calculable: true,
+      inRange: {
+        color: ['lightskyblue', 'yellow', 'orangered']
+      }
+    },
+    series: [
+      {
+        type: 'map',
+        map: 'ShanXi',
+        label: {
+          show: true
+        },
+        data: list.map(v => {
+          return {
+            name: v.category,
+            value: v.ct,
+            distribution: ((v.ct / t) * 100).toFixed(2) - 0 + '%'
+          };
+        })
+      }
+    ]
+  });
+};
+
+let phoneChart = undefined;
+const createPhone = (list = []) => {
+  if (!list || list.length == 0) return;
+  phoneChart = echarts.init(tag.value);
+  phoneChart.resize({
+    height: (tag.value.offsetWidth / 16) * 9
+  });
+  const keys = [],
+    values = [];
+  for (let i = 0; i < list.length; i++) {
+    const v = list[i];
+    keys.push(v.category);
+    values.push(v.ct);
+  }
+  phoneChart.setOption({
+    title: {},
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      }
+    },
+    legend: {
+      itemWidth: 10,
+      itemHeight: 10,
+      itemGap: 30,
+      left: '15%',
+      top: '3%',
+      textStyle: {
+        color: '#9B9B9B'
+      }
+    },
+    grid: {
+      left: '10%',
+      right: '10%',
+      top: '10%',
+      bottom: '10%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'value',
+      boundaryGap: [0, 0.01],
+      splitLine: { show: true },
+      axisLabel: {
+        textStyle: {
+          color: '#9B9B9B'
+        }
+      }
+    },
+    yAxis: {
+      type: 'category',
+      data: keys,
+      splitLine: { show: false },
+      axisLabel: {
+        textStyle: {
+          color: '#9B9B9B'
+        }
+      },
+      axisTick: { show: false }
+    },
+    series: [
+      {
+        type: 'bar',
+        data: values,
+        itemStyle: {
+          color: 'rgba(255,77,79,0.35)',
+          borderColor: '#FF4D4F'
+        },
+        label: {
+          formatter: data => {
+            return numform(data.data);
+          },
+          show: true,
+          position: 'right',
+          textStyle: {
+            fontFamily: 'Noto Sans SC',
+            fontSize: 14,
+            color: '#000'
+          }
+        }
+      }
+    ]
+  });
+};
+
+let categoryChart = undefined;
+const createCategory = (list = []) => {
+  if (!list || list.length == 0) return;
+  categoryChart = echarts.init(category.value);
+  categoryChart.resize({
+    height: category.value.offsetWidth
+  });
+  const keys = [],
+    values = [];
+  for (let i = 0; i < list.length; i++) {
+    const v = list[i];
+    keys.push(v.category);
+    values.push(v.ct);
+  }
+  categoryChart.setOption({
+    title: {},
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      }
+    },
+    legend: {
+      itemWidth: 10,
+      itemHeight: 10,
+      itemGap: 30,
+      left: '15%',
+      top: '3%',
+      textStyle: {
+        color: '#9B9B9B'
+      }
+    },
+    grid: {
+      left: '10%',
+      right: '10%',
+      top: '10%',
+      bottom: '10%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'value',
+      boundaryGap: [0, 0.01],
+      splitLine: { show: true },
+      axisLabel: {
+        textStyle: {
+          color: '#9B9B9B'
+        }
+      }
+    },
+    yAxis: {
+      type: 'category',
+      data: keys,
+      splitLine: { show: false },
+      axisLabel: {
+        textStyle: {
+          color: '#9B9B9B'
+        }
+      },
+      axisTick: { show: false }
+    },
+    series: [
+      {
+        type: 'bar',
+        data: values,
+        itemStyle: {
+          color: 'rgba(255,77,79,0.35)',
+          borderColor: '#FF4D4F'
+        },
+        label: {
+          formatter: data => {
+            return numform(data.data);
+          },
+          show: true,
+          position: 'right',
+          textStyle: {
+            fontFamily: 'Noto Sans SC',
+            fontSize: 14,
+            color: '#000'
+          }
+        }
+      }
+    ]
+  });
+};
+
+const getData = () => {
+  Promise.all([
+    getSexData({ appid: appSelect.value }),
+    getAgeData({ appid: appSelect.value }),
+    getCityData({ appid: appSelect.value }),
+    getTagData({ appid: appSelect.value }),
+    getPhoneData({ appid: appSelect.value }),
+    getCategoryData({ appid: appSelect.value })
+  ]).then(list => {
+    /**
+     * list[2] 地区分布 地形图
+     * list[3] 标签分布 柱状图
+     */
+    createSex(list[0]);
+    createAge(list[1]);
+    createCity(list[2]);
+    createTag(list[3]);
+    createPhone(list[4]);
+    createCategory(list[5]);
+  });
+};
+</script>
+
+<style scoped></style>