Parcourir la source

reactor:新增宜宾市司法行政执法监督子系统大屏;

zhangchong il y a 1 an
Parent
commit
302c480b12

+ 0 - 1
.gitignore

@@ -25,4 +25,3 @@ coverage
 *.ntvs*
 *.njsproj
 *.sln
-*.sw?

+ 12 - 4
src/router/index.ts

@@ -15,11 +15,19 @@ const routes: Array<RouteRecordRaw> = [
         }
     },
     {
-        path: '/seats',
-        name: 'seats',
-        component: () => import('@/views/seats/index.vue'),
+        path: '/home',
+        name: 'home',
+        component: () => import('@/views/home/index.vue'),
+        meta: {
+            title: '宜宾市12345政务服务便民热线',
+        }
+    },
+    {
+        path: '/judicial',
+        name: 'judicial',
+        component: () => import('@/views/judicial/index.vue'),
         meta: {
-            title: '宜宾市12345坐席监控中心',
+            title: '宜宾市司法行政执法监督子系统',
         }
     },
     {

+ 155 - 0
src/views/judicial/center-bottom.vue

@@ -0,0 +1,155 @@
+<template>
+  <div class="center_bottom">
+    <div class="center_bottom-title flex">
+      <div class="flex items-center">
+        <img src="@/assets/img/home/title_arrow.png" alt="">
+        行政执法工单概览
+      </div>
+    </div>
+    <div class="center_bottom-content">
+      <div class="table-header">
+        <div class="table-header-item">超期状态</div>
+        <div class="table-header-item">来源</div>
+        <div class="table-header-item">标题</div>
+        <div class="table-header-item">受理类型</div>
+        <div class="table-header-item">热点类型</div>
+        <div class="table-header-item">区域</div>
+        <div class="table-header-item">接办部门</div>
+      </div>
+      <div class="scroll" v-loading="loading">
+        <vue3-seamless-scroll :list="list" hover :singleHeight="100" v-if="list.length">
+          <div class="item" v-for="(item, index) in list" :key="index">
+            <span>
+              <span class="exceedSoon" v-if="item.expiredStatus === 1">{{ item.expiredStatusText }}</span>
+              <span class="exceed" v-if="item.expiredStatus === 2">{{ item.expiredStatusText }}</span>
+              <span class="normal" v-if="item.expiredStatus === 0">{{ item.expiredStatusText }}</span>
+            </span>
+            <span>{{ item.sourceChannel }}</span>
+            <el-tooltip placement="top">
+              <template #content> {{ item.title }}</template>
+              <span>{{ item.title }}</span>
+            </el-tooltip>
+            <span :title="item.acceptType">{{ item.acceptType }}</span>
+            <span :title="item.hotspotName">{{ item.hotspotName }}</span>
+            <span :title="item.county">{{ item.county }}</span>
+            <span :title="item.actualHandleOrgName">{{ item.actualHandleOrgName }}</span>
+          </div>
+        </vue3-seamless-scroll>
+        <template v-else>
+          <EmptyCom></EmptyCom>
+        </template>
+      </div>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import {ref, onMounted, onUnmounted} from "vue";
+import {Vue3SeamlessScroll} from "vue3-seamless-scroll";
+import {orderView} from "api/home";
+import mittBus from "@/utils/mitt";
+import EmptyCom from "@/components/empty-com";
+
+const list = ref([])
+const loading = ref(false);
+const getData = async () => {
+  loading.value = true;
+  try {
+    const {result} = await orderView();
+    list.value = result;
+    loading.value = false;
+  } catch (e) {
+    console.log(e);
+    loading.value = false;
+  }
+}
+onMounted(() => {
+  // 接收消息
+  mittBus.on('orderHandlingDetail', (res: any) => {
+    list.value = res;
+  });
+  getData();
+})
+onUnmounted(() => {
+  // 取消接收消息
+  mittBus.off('orderHandlingDetail');
+})
+</script>
+
+<style scoped lang="scss">
+.center_bottom {
+  padding: 0 30px;
+
+  &-title {
+    font-size: 20px;
+    color: #fff;
+    justify-content: space-between;
+    align-items: center;
+  }
+
+  &-content {
+    margin-top: 10px;
+
+    .table-header {
+      display: flex;
+      justify-content: space-between;
+      background: linear-gradient(to right, rgba(27, 51, 55, .8), rgba(45, 83, 91, .8));
+
+      width: 100%;
+      height: 40px;
+      line-height: 40px;
+
+      .table-header-item {
+        text-align: center;
+        color: #B4D7EC;
+        flex: 1;
+      }
+    }
+
+    .scroll {
+      height: 200px;
+      overflow: hidden;
+      margin-top: 10px;
+
+      .item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        border-bottom: 1px solid #225073;
+        padding: 6px 0;
+
+        span {
+          display: inline-block;
+          white-space: nowrap;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          text-align: center;
+          color: #CFD2D2;
+          flex: 1;
+        }
+
+        .exceedSoon {
+          border: 1px solid #ECA455;
+          border-radius: 3px;
+          color: #ECA455;
+          padding: 2px 5px;
+        }
+
+        .exceed {
+          border: 1px solid #D70024;
+          border-radius: 3px;
+          color: #D70024;
+          padding: 2px 5px;
+        }
+
+        .normal {
+          border: 1px solid #69BBF6;
+          border-radius: 3px;
+          color: #69BBF6;
+          padding: 2px 5px;
+        }
+      }
+    }
+
+  }
+}
+</style>

+ 199 - 0
src/views/judicial/center-map.vue

@@ -0,0 +1,199 @@
+<template>
+  <div class="center-map">
+    <v-chart
+        class="chart"
+        :option="option"
+        ref="centerMapRef"
+        :loading="loading"
+        :loading-options="loadingOptions"
+    />
+  </div>
+</template>
+<script setup lang="ts">
+import {ref, onMounted, watch, nextTick} from "vue";
+import {registerMap, getMap} from "echarts/core";
+import {optionHandle} from "./center.map";
+import {loopShowTooltip} from "@/utils/tooltip-auto-show";
+import axios from "axios";
+import {areaDetail} from "api/home";
+import dayjs from "dayjs";
+import {loadingOptions} from "@/utils/constants";
+
+const props = defineProps({
+  dateArray: {
+    type: Array,
+    default: () => []
+  }
+})
+const date = ref([]);
+watch(() => props.dateArray, (val: any) => {
+  date.value = val;
+}, {immediate: true})
+
+watch(() => props.dateArray, (val: any) => {
+  getData();
+})
+const loading = ref(false);
+const option = ref({});
+const code = ref("511500"); //100000 代表中国 其他地市是行政编码
+const dataSetHandle = async (regionCode: string, list: object[]) => {
+  code.value = regionCode;
+  const geoJson: any = await getGeoJson(regionCode);
+  let cityCenter: any = {};
+  //获取当前地图每块行政区中心点
+  geoJson.features.forEach((element: any) => {
+    cityCenter[element.properties.name] =
+        element.properties.centroid || element.properties.center;
+  });
+  option.value = optionHandle(regionCode, list);
+};
+
+const getGeoJson = (regionCode: string) => {
+  return new Promise<boolean>(async (resolve) => {
+    let mapJson = getMap(regionCode);
+    if (mapJson) {
+      mapJson = mapJson.geoJSON;
+      resolve(mapJson);
+    } else {
+      mapJson = await axios.get(`./map-geojson/${regionCode}.json`).then(
+          (data) => data.data
+      );
+      code.value = regionCode;
+      registerMap(regionCode, {
+        geoJSON: mapJson as any,
+        specialAreas: {},
+      });
+      resolve(mapJson);
+    }
+  });
+};
+// 获取数据设置到地图中
+const getData = async () => {
+  loading.value = true;
+  try {
+    const {result} = await areaDetail({
+      StartDate: dayjs(date.value[0]).format('YYYY-MM-DD'),
+      EndDate: dayjs(date.value[1]).format('YYYY-MM-DD')
+    });
+    const regionData = result.map((item: any) => {
+      return {
+        name: item.areaName,
+        ...item
+      }
+    })
+    await dataSetHandle(code.value, regionData);
+    loading.value = false;
+  } catch (e) {
+    console.log(e);
+    loading.value = false;
+  }
+};
+const tooltipTimer = ref<any>(null);
+const centerMapRef = ref<any>(null);
+const tooltipMap = (myChart, option) => {
+  tooltipTimer.value && tooltipTimer.value.clearLoop(); // this.tooltipTimer 在data里定义
+  tooltipTimer.value = 0;
+  // 调用轮播的方法
+  // myChart为Echarts容器实例
+  tooltipTimer.value = loopShowTooltip(myChart, option, {
+    interval: 5000, // 轮播间隔时间
+    loopSeries: false, // 是否开启轮播循环
+    seriesIndex: 0, // 开始轮播的系列索引
+    // shunXu: "daoXu", // 显示顺序
+    // loopSeries: boolean类型,默认为false。true表示循环所有series的tooltip;false则显示指定seriesIndex的tooltip。
+    // seriesIndex: 默认为0,指定某个系列(option中的series索引)循环显示tooltip,当loopSeries为true时,从seriesIndex系列开始执行。
+  });
+};
+onMounted(async () => {
+  await getData();
+  await nextTick(() => {
+    tooltipMap(centerMapRef.value!.chart, option.value);
+  });
+});
+</script>
+
+<style scoped lang="scss">
+.center-map {
+  height: 650px;
+  width: 100%;
+  box-sizing: border-box;
+  position: relative;
+}
+</style>
+<style lang="scss">
+.custom-tooltip-box {
+  padding: 0 !important;
+  border: none !important;
+  background-color: transparent !important;
+  // 给子盒子自定义样式
+  .custom-tooltip-style {
+    width: 355px;
+    height: 219px;
+    background-image: url('@/assets/img/home/tool_tip_bg.png');
+    background-size: 100% 100%;
+    color: #fff;
+    padding: 20px 30px;
+
+    .custom-tooltip-name {
+      font-size: 20px;
+      font-weight: bold;
+      margin-bottom: 15px;
+      display: flex;
+      align-items: center;
+
+      .custom-tooltip-arrow {
+        display: inline-block;
+        width: 18px;
+        height: 18px;
+        background-image: url("@/assets/img/home/tool_tip_arrow.png");
+        margin-right: 10px;
+      }
+    }
+
+    .custom-tooltip-style-box {
+      display: flex;
+      color: #d5e7e7;
+      font-size: 14px;
+
+      .custom-tooltip-style-box-text {
+        flex: 1;
+        margin-left: 10px;
+
+        .custom-tooltip-style-box-text-item {
+          margin-bottom: 8px;
+          display: flex;
+          align-items: center;
+
+          &-value {
+            display: inline-block;
+            color: #fff;
+            font-weight: bold;
+            max-width: 90px;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            font-size: 18px;
+            margin-right: 2px;
+          }
+
+          &-title {
+            display: inline-block;
+            width: 180px;
+            overflow: hidden;
+            text-overflow: ellipsis;
+          }
+
+          .last {
+            font-size: 14px;
+          }
+
+          b {
+            color: #fff;
+            font-weight: normal;
+          }
+        }
+      }
+
+    }
+  }
+}
+</style>

+ 185 - 0
src/views/judicial/center.map.ts

@@ -0,0 +1,185 @@
+import {getImageUrl} from "@/utils/tools";
+
+const arrow = new Image();
+arrow.src = getImageUrl("home/map_select.png");
+const mapBg = new Image();
+mapBg.src = getImageUrl("home/map_bg.png");
+const onetow = new Image();
+onetow.src = getImageUrl("home/123.png");
+export const optionHandle = (regionCode: string,
+                             list: object[]) => {
+    return {
+        backgroundColor: "rgba(0,0,0,0)",
+        tooltip: {
+            trigger: 'item',
+            position: function (point: any[], params: any, dom: any, rect: any, size: any) {
+                let x = 0; // x坐标位置
+                let y = 0; // y坐标位置
+                // 当前鼠标位置
+                const pointX = point[0];
+                const pointY = point[1];
+                // 提示框大小
+                const boxWidth = size.contentSize[0];
+                const boxHeight = size.contentSize[1];
+
+                // boxWidth > pointX 说明鼠标左边放不下提示框
+                if (boxWidth > pointX) {
+                    x = pointX + 10;
+                } else { // 左边放的下
+                    x = pointX - boxWidth - 10;
+                }
+
+                // boxHeight > pointY 说明鼠标上边放不下提示框
+                if (boxHeight > pointY) {
+                    y = 5;
+                } else { // 上边放得下
+                    y = pointY - boxHeight;
+                }
+                return [x, y];
+            },
+            show: true,
+            textStyle: {
+                fontSize: 14,
+                color: '#fff',
+            },
+            className: 'custom-tooltip-box',
+            rich: {
+                arrow: {
+                    backgroundColor: {
+                        image: getImageUrl("home/tool_tip_arrow.png")
+                    },
+                }
+            },
+            formatter: function (params: any) {
+                let tipHtml = '';
+                tipHtml = `
+                    <div class="custom-tooltip-style">
+                        <div class='custom-tooltip-name'> <span class="custom-tooltip-arrow"></span> ${params.name}</div>
+                        <div class='custom-tooltip-style-box'>
+                            <div class="custom-tooltip-style-box-text">
+                                <div class="custom-tooltip-style-box-text-item"><span class="custom-tooltip-style-box-text-item-title">工单量:</span><span class="custom-tooltip-style-box-text-item-value">${params.data.handlingCount}</span> <b> 件</b></div>
+                                <div class="custom-tooltip-style-box-text-item"><span class="custom-tooltip-style-box-text-item-title">行政执法工单:</span><span class="custom-tooltip-style-box-text-item-value">${params.data.filedCount}</span> <b> 件</b></div>
+                                <div class="custom-tooltip-style-box-text-item"><span class="custom-tooltip-style-box-text-item-title">线索属实工单:</span><span class="custom-tooltip-style-box-text-item-value">${params.data.overTimeCount}</span> <b> 件</b></div>
+                                <div class="custom-tooltip-style-box-text-item"><span class="custom-tooltip-style-box-text-item-title">线索不属实工单:</span><span class="custom-tooltip-style-box-text-item-value">${params.data.overTimeCount}</span> <b> 件</b></div>
+                                <div class="custom-tooltip-style-box-text-item"><span class="custom-tooltip-style-box-text-item-title">推诿工单:</span><span class="custom-tooltip-style-box-text-item-value">${params.data.overTimeCount}</span> <b> 件</b></div>
+                            </div>
+                        </div>
+                    </div>
+                `
+                return tipHtml;
+            },
+        },
+        geo: [
+            {
+                layoutCenter: ['50%', '50%'],//位置
+                layoutSize: '180%',//大小
+                show: true,
+                map: regionCode,
+                roam: false,
+                zoom: 0.65,
+                aspectScale: 1,
+                label: {
+                    show: false,
+                },
+                itemStyle: {
+                    areaColor: {
+                        type: "linear",
+                        x: 1200,
+                        y: 0,
+                        x2: 0,
+                        y2: 0,
+                        colorStops: [{
+                            offset: 0,
+                            color: "rgba(38,134,132,0.35)", // 0% 处的颜色
+                        }, {
+                            offset: 1,
+                            color: "rgba(38,134,132,0.75)", // 50% 处的颜色
+                        },],
+                        global: true, // 缺省为 false
+                    },
+                    borderColor: "#c0f3fb",
+                    borderWidth: 1,
+                    shadowColor: "#8cd3ef",
+                    shadowOffsetY: 10,
+                    shadowBlur: 120,
+                }
+            },
+            {
+                type: "map",
+                map: regionCode,
+                aspectScale: 1,
+                zoom: 0.65,
+                layoutCenter: ["50%", "54%"],
+                layoutSize: "180%",
+                roam: false,
+                silent: true,
+                itemStyle: {
+                    borderWidth: 2,
+                    borderColor: "rgba( 38,134,132,0.8)",
+                    shadowColor: "rgba(29, 111, 165,0.8)",
+                    shadowOffsetY: 10,
+                    shadowBlur: 2,
+                    areaColor: "rgba(5,21,35,0.2)",
+                    borderType: "dashed",
+                },
+            },
+        ],
+        geo3D: [
+            {
+                show: false,
+                map: regionCode,
+                aspectScale: 1,
+            }
+        ],
+        series: [
+            {
+                type: "map",
+                map: regionCode, // 自定义扩展图表类型
+                aspectScale: 1,
+                zoom: 0.65,
+                showLegendSymbol: true,
+                boxDepth: 120, //地图倾斜度
+                regionHeight: 3, //地图厚度
+                label: {
+                    show: false,
+                },
+                itemStyle: {
+                    color: "rgba( 73,122,105,.3)",
+                    borderColor: "#3CFFFD",
+                    borderWidth: 1,
+                    areaColor: {
+                        color: "rgba( 73,122,105,.3)",
+                    }
+
+                },
+                emphasis: { // 选中的样式
+                    label: {
+                        formatter: [
+                            '{b|}',
+                        ].join('\n'),
+
+                        rich: {
+                            b: {
+                                backgroundColor: {
+                                    image: arrow,
+                                },
+                                height: 60
+                            },
+                        }
+                    },
+                    itemStyle: {
+                        borderColor: "rgba( 62, 254, 254,.3)",
+                        borderWidth: 2,
+                        areaColor: 'rgba( 62, 254, 254,.3)',
+                    }
+                },
+                layoutCenter: ["50%", "50%"],
+                layoutSize: "180%",
+                data: list,
+                select: {
+                    disabled: true,
+                },
+            },
+        ],
+    };
+}

+ 97 - 0
src/views/judicial/container.vue

@@ -0,0 +1,97 @@
+<template>
+  <div class="container-box">
+    <div class="container-left">
+      <LeftTop class="container-left-top" :dateArray="dateArray"/>
+      <LeftCenter class="container-left-center" :dateArray="dateArray"/>
+      <LeftBottom class="container-left-bottom" :dateArray="dateArray"/>
+    </div>
+    <div class="container-center">
+      <CenterMap class="container-center-map" :dateArray="dateArray"/>
+      <CenterBottom class="container-center-bottom" :dateArray="dateArray"/>
+    </div>
+    <div class="container-right">
+      <RightTop class="container-right-top" :dateArray="dateArray"/>
+      <RightCenter class="container-right-center"/>
+      <RightBottom class="container-right-bottom" :dateArray="dateArray"/>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import LeftTop from "@/views/judicial/left-top.vue";
+import LeftCenter from "@/views/judicial/left-center.vue";
+import LeftBottom from "@/views/judicial/left-bottom.vue";
+import CenterMap from "@/views/judicial/center-map.vue";
+import CenterBottom from "@/views/judicial/center-bottom.vue";
+import RightTop from "@/views/judicial/right-top.vue";
+import RightCenter from "@/views/judicial/right-center.vue";
+import RightBottom from "@/views/judicial/right-bottom.vue";
+
+const props = defineProps({
+  dateArray: {
+    type: Array,
+    default: () => []
+  }
+})
+</script>
+<style scoped lang="scss">
+.container-box {
+  width: 100%;
+  display: flex;
+  min-height: calc(100% - 100px);
+  justify-content: space-between;
+  margin-top: 10px;
+
+  .container-left, .container-right {
+    display: flex;
+    flex-direction: column;
+    position: relative;
+    width: 540px;
+    box-sizing: border-box;
+    flex-shrink: 0;
+
+    .container-left-top {
+      height: 350px;
+    }
+
+    .container-left-center {
+      height: 305px;
+    }
+
+    .container-left-bottom {
+      height: 290px;
+    }
+  }
+
+  .container-center-float {
+    position: absolute;
+    top: 300px;
+    left: 520px;
+    z-index: 2;
+  }
+
+  .container-center {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+
+    .center_center-bottom {
+      height: 315px;
+    }
+  }
+
+  .container-right, .container-left {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    position: relative;
+    width: 540px;
+    box-sizing: border-box;
+    flex-shrink: 0;
+
+    .container-right-top, .container-right-center, .container-right-bottom {
+      height: 300px;
+      overflow: hidden;
+    }
+  }
+}
+</style>

+ 98 - 0
src/views/judicial/header.vue

@@ -0,0 +1,98 @@
+<template>
+  <div class="d-flex jc-center title_wrap">
+    <div class="time-picker">
+      <el-date-picker
+          v-model="timeValue"
+          type="daterange"
+          unlink-panels
+          range-separator="至"
+          start-placeholder="开始时间"
+          end-placeholder="结束时间"
+          :shortcuts="shortcuts"
+          :clearable="false"
+          @change="changeDate"
+      />
+    </div>
+    <div class="d-flex jc-center">
+      <div class="title">
+        <span class="title-text">{{ title }}</span>
+      </div>
+    </div>
+    <div class="timers">
+      {{ dateData.dateDay }} {{ dateData.dateWeek }}
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import {reactive, ref} from "vue";
+import dayjs from "dayjs";
+import {shortcuts} from '@/utils/constants';
+
+const emit = defineEmits(["changeDate"]);
+
+const title = ref("宜宾市司法行政执法监督子系统");
+const timeValue = ref<any>([
+  dayjs().subtract(1, "month").toDate(),
+  dayjs().toDate(),
+]); //默认近一个月
+emit("changeDate", timeValue.value);
+const changeDate = (val: any) => {
+  emit("changeDate", val);
+};
+const dateData = reactive<any>({
+  dateDay: "",
+  dateYear: "",
+  dateWeek: "",
+  timing: null as any,
+});
+
+const weekday = ["星期天", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
+const timeFn = () => {
+  dateData.timing = setInterval(() => {
+    dateData.dateDay = dayjs().format("YYYY-MM-DD HH:mm:ss");
+    dateData.dateWeek = weekday[dayjs().day()];
+  }, 1000);
+};
+timeFn();
+</script>
+
+<style scoped lang="scss">
+.title_wrap {
+  height: 90px;
+  background-size: cover;
+  background-position: center center;
+  position: relative;
+
+  .time-picker {
+    position: absolute;
+    left: 30px;
+    top: 40px;
+    font-size: 18px;
+    z-index: 9;
+  }
+
+  .timers {
+    position: absolute;
+    right: 30px;
+    top: 40px;
+    font-size: 18px;
+    display: flex;
+    align-items: center;
+  }
+}
+
+.title {
+  position: relative;
+  text-align: center;
+  height: 90px;
+  line-height: 90px;
+
+  .title-text {
+    font-size: 32px;
+    font-weight: 900;
+    letter-spacing: 2px;
+    width: 100%;
+    color: #fff;
+  }
+}
+</style>

+ 46 - 0
src/views/judicial/index.vue

@@ -0,0 +1,46 @@
+<template>
+  <scale-screen
+      width="1920"
+      height="1080"
+      :delay="500"
+      :fullScreen="true"
+      :boxStyle="{
+      background: '#03050C',
+      overflow: isScale ? 'hidden' : 'auto',
+    }"
+      :wrapperStyle="wrapperStyle"
+      :autoScale="isScale"
+  >
+    <div class="home-content-wrap">
+      <Headers @changeDate="changeDate"/>
+      <Container :dateArray="dateArray"/>
+    </div>
+  </scale-screen>
+</template>
+<script setup lang="ts" name="judicial">
+import {defineAsyncComponent, ref} from "vue";
+import {useSettingStore} from "@/stores/setting";
+import {storeToRefs} from "pinia";
+
+const ScaleScreen = defineAsyncComponent(() => import('@/components/scale-screen'));
+const Headers = defineAsyncComponent(() => import('@/views/judicial/header.vue'));
+const Container = defineAsyncComponent(() => import('@/views/judicial/container.vue'));
+
+const settingStore = useSettingStore();
+const {isScale} = storeToRefs(settingStore);
+const wrapperStyle = {};
+const dateArray = ref([]);
+const changeDate = (date: any[]) => {
+  dateArray.value = date;
+};
+</script>
+
+<style scoped lang="scss">
+.home-content-wrap {
+  width: 100%;
+  height: 100%;
+  box-sizing: border-box;
+  background-image: url("@/assets/img/home/bg.png");
+  background-size: 100% 100%;
+}
+</style>

+ 335 - 0
src/views/judicial/left-bottom.vue

@@ -0,0 +1,335 @@
+<template>
+  <div class="left_bottom">
+    <div class="left_bottom-title flex">
+      <div class="flex items-center">
+        <img src="@/assets/img/home/title_arrow.png" alt="">
+        高频事项分析
+      </div>
+      <el-dropdown @command="handleCommand">
+        <el-button link class="link-button">
+          {{ areaText }}
+          <el-icon class="el-icon--right">
+            <arrow-down/>
+          </el-icon>
+        </el-button>
+        <template #dropdown>
+          <el-dropdown-menu>
+            <el-dropdown-item v-for="item in areaList" :key="item.value" :command="item">{{ item.name }}</el-dropdown-item>
+          </el-dropdown-menu>
+        </template>
+      </el-dropdown>
+    </div>
+    <div class="left_bottom-content">
+      <template v-if="list.length">
+        <v-chart class="chart" :option="option" :loading="loading" :loading-options="loadingOptions"/>
+      </template>
+      <template v-else>
+        <EmptyCom></EmptyCom>
+      </template>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import {onMounted, ref, watch} from "vue";
+import {ArrowDown} from '@element-plus/icons-vue'
+import {hotSpot} from "@/api/home";
+import dayjs from "dayjs";
+import EmptyCom from "@/components/empty-com";
+import {loadingOptions} from "@/utils/constants";
+import {graphic} from "echarts/core";
+import {arraySortByKey} from "@/utils/tools";
+
+const props = defineProps({
+  dateArray: {
+    type: Array,
+    default: () => []
+  }
+})
+
+const areaList = ref([
+  {
+    name: '宜宾市',
+    value: '511500'
+  },
+  {
+    name: '翠屏区',
+    value: '511502'
+  },
+  {
+    name: '南溪区',
+    value: '511503'
+  },
+  {
+    name: '叙州区',
+    value: '511504'
+  },
+  {
+    name: '江安县',
+    value: '511523'
+  },
+  {
+    name: '长宁县',
+    value: '511524'
+  },
+  {
+    name: '高县',
+    value: '511525'
+  },
+  {
+    name: '珙县',
+    value: '511526'
+  },
+  {
+    name: '筠连县',
+    value: '511527'
+  },
+  {
+    name: '兴文县',
+    value: '511528'
+  },
+  {
+    name: '屏山县',
+    value: '511529'
+  }
+]);
+const date = ref([]);
+const loading = ref(false);
+const option = ref<any>({});
+watch(() => props.dateArray, (val: any) => {
+  date.value = val;
+}, {immediate: true})
+
+watch(() => props.dateArray, () => {
+  getData();
+})
+const areaCode = ref('511500');
+const areaText = ref('宜宾市');
+const list = ref<any>([]);
+const getData = async () => {
+  loading.value = true;
+  try {
+    const {result} = await hotSpot({
+      StartDate: dayjs(date.value[0]).format('YYYY-MM-DD'),
+      EndDate: dayjs(date.value[1]).format('YYYY-MM-DD'),
+      AreaCode: areaCode.value
+    });
+    list.value = result;
+
+    const category = arraySortByKey(result, 'sumCount',);
+
+    const charts = { // 按顺序排列从大到小
+      cityList: category.map((item: any) => item.hotspotName),
+      cityData: category.map((item: any) => item.sumCount)
+    };
+    const top10CityList = charts.cityList;
+    const top10CityData = charts.cityData;
+    const color = ['#ff9500', '#02d8f9', '#027fff'];
+    const color1 = ['#ffb349', '#70e9fc', '#4aa4ff'];
+
+    let lineY = []
+    let lineT = []
+    for (let i = 0; i < charts.cityList.length; i++) {
+      let x = i;
+      if (x > 1) {
+        x = 2
+      }
+      const data = {
+        name: charts.cityList[i],
+        color: color[x],
+        value: top10CityData[i],
+        barGap: '-100%',
+        itemStyle: {
+          show: true,
+          color: new graphic.LinearGradient(0, 0, 1, 0, [{
+            offset: 0,
+            color: color[x]
+          }, {
+            offset: 1,
+            color: color1[x]
+          }], false),
+          borderRadius: 10,
+        },
+        emphasis: {
+          itemStyle: {
+            shadowBlur: 15,
+            shadowColor: 'rgba(0, 0, 0, 0.1)'
+          }
+        }
+      };
+      const data1 = {
+        value: top10CityData[0],
+        itemStyle: {
+          color: '#001235',
+          borderRadius: 10
+        }
+      };
+      lineY.push(data)
+      lineT.push(data1)
+    }
+
+    option.value = {
+      backgroundColor: 'rgba(0, 0, 0, 0)',
+      title: {
+        show: false
+      },
+      tooltip: {},
+      grid: {
+        borderWidth: 0,
+        top: '5%',
+        left: '5%',
+        right: '15%',
+        bottom: '10%'
+      },
+      color: color,
+      yAxis: [{
+        type: 'category',
+        inverse: true,
+        axisTick: {
+          show: false
+        },
+        axisLine: {
+          show: false
+        },
+        axisLabel: {
+          show: false,
+          inside: false
+        },
+        data: top10CityList
+      }, {
+        type: 'category',
+        inverse: true,
+        axisLine: {
+          show: false
+        },
+        axisTick: {
+          show: false
+        },
+        axisLabel: {
+          show: true,
+          inside: false,
+          verticalAlign: 'middle',
+          lineHeight: '40',
+          color: '#fff',
+          fontSize: '14',
+          fontFamily: 'PingFangSC-Regular',
+          formatter: function (val) {
+            return `${val}`
+          }
+        },
+        splitArea: {
+          show: false
+        },
+        splitLine: {
+          show: false
+        },
+        data: top10CityData
+      }],
+      xAxis: {
+        type: 'value',
+        axisTick: {
+          show: false
+        },
+        axisLine: {
+          show: false
+        },
+        splitLine: {
+          show: false
+        },
+        axisLabel: {
+          show: false
+        }
+      },
+      series: [{
+        name: 'total',
+        type: 'bar',
+        zLevel: 1,
+        barGap: '-100%',
+        barWidth: '10px',
+        data: lineT,
+        legendHoverLink: false
+      }, {
+        type: 'bar',
+        zLevel: 2,
+        barWidth: '10px',
+        data: lineY,
+        label: {
+          color: '#b3ccf8',
+          show: true,
+          position: [0, '-18px'],
+          fontSize: 16,
+          formatter: function (a) {
+            let num = ''
+            let str = ''
+            if (a.dataIndex + 1 < 10) {
+              num = '0' + (a.dataIndex + 1);
+            } else {
+              num = (a.dataIndex + 1);
+            }
+            if (a.dataIndex === 0) {
+              str = `{color1|${num}} {color4|${a.name}}`
+            } else if (a.dataIndex === 1) {
+              str = `{color2|${num}} {color4|${a.name}}`
+            } else {
+              str = `{color3|${num}} {color4|${a.name}}`
+            }
+            return str;
+          },
+          rich: {
+            color1: {
+              color: '#ff9500',
+              fontWeight: 700
+            },
+            color2: {
+              color: '#02d8f9',
+              fontWeight: 700
+            },
+            color3: {
+              color: '#027fff',
+              fontWeight: 700
+            },
+            color4: {
+              color: '#e5eaff'
+            }
+          }
+        }
+      }],
+    }
+    loading.value = false;
+  } catch (e) {
+    console.log(e);
+    loading.value = false;
+  }
+}
+// 选择省市区
+const handleCommand = (command: any) => {
+  areaCode.value = command.value;
+  areaText.value = command.name;
+  getData();
+}
+onMounted(() => {
+  getData();
+});
+</script>
+<style scoped lang="scss">
+.left_bottom {
+  padding: 0 30px;
+
+  &-title {
+    font-size: 20px;
+    color: #fff;
+    justify-content: space-between;
+    align-items: center;
+  }
+
+  &-content {
+    height: calc(100% - 30px);
+  }
+}
+
+:deep(.link-button) {
+  cursor: pointer;
+  color: #7DBDEC;
+  display: flex;
+  align-items: center;
+}
+</style>

+ 202 - 0
src/views/judicial/left-center.vue

@@ -0,0 +1,202 @@
+<template>
+  <div class="left-center">
+    <div class="left-center-title flex items-center">
+      <img src="@/assets/img/home/title_arrow.png" alt="">
+      受理类型办件分析
+    </div>
+    <div class="left-center-content">
+      <template v-if="xData.length">
+        <v-chart class="chart" :option="option" :loading="loading" :loading-options="loadingOptions"/>
+      </template>
+      <template v-else>
+        <EmptyCom></EmptyCom>
+      </template>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import {ref, onMounted, watch} from "vue";
+import {acceptType} from "@/api/home";
+import dayjs from "dayjs";
+import EmptyCom from "@/components/empty-com";
+import {loadingOptions} from "@/utils/constants";
+
+const props = defineProps({
+  dateArray: {
+    type: Array,
+    default: () => []
+  }
+})
+
+const date = ref([]);
+watch(() => props.dateArray, (val: any) => {
+  date.value = val;
+}, {immediate: true})
+
+watch(() => props.dateArray, (val: any) => {
+  getData();
+})
+
+
+const option = ref({});
+const xData = ref([]);
+const loading = ref(true);
+const getData = async () => {
+  loading.value = true;
+  try {
+    const {result} = await acceptType({StartDate: dayjs(date.value[0]).format('YYYY-MM-DD'), EndDate: dayjs(date.value[1]).format('YYYY-MM-DD')});
+    xData.value = result.map((item: any) => item.acceptType);
+    const totalData = result.reduce((pre: any, cur: any) => {
+      pre.push(cur.sumCount);
+      return pre;
+    }, []);
+    const handlingCount = result.reduce((pre: any, cur: any) => {
+      pre.push(cur.handlingCount);
+      return pre;
+    }, []);
+    const filedCount = result.reduce((pre: any, cur: any) => {
+      pre.push(cur.filedCount);
+      return pre;
+    }, []);
+    const overTimeCount = result.reduce((pre: any, cur: any) => {
+      pre.push(cur.overTimeCount);
+      return pre;
+    }, []);
+    // mock 数据
+    let dataArr = {
+      xData: xData.value,
+      result: [
+        {name: '总数', data: totalData},
+        {name: '已办', data: handlingCount},
+        {name: '在办', data: filedCount},
+        {name: '超期', data: overTimeCount},
+      ],
+      series: []
+    }
+    const diamondData = dataArr.result.reduce((pre, cur, index) => {
+      pre[index] = cur.data.map((el, id) => el + (pre[index - 1] ? pre[index - 1][id] : 0))
+      return pre
+    }, [])
+    const color = [
+      [{offset: 0, color: "#efff37",}, {offset: 1, color: "#d5e700",}],
+      [{offset: 0, color: "#32ffee",}, {offset: 1, color: "#00e8d5",}],
+      [{offset: 0, color: "#46c9ff",}, {offset: 1, color: "#00b4ff",}],
+      [{offset: 0, color: "#54a0ff",}, {offset: 1, color: "#1f83ff",}],
+    ]
+
+    dataArr.series = dataArr.result.reduce((p, c, i, array) => {
+      p.push({
+        z: i + 1,
+        stack: true,
+        type: 'bar',
+        name: c.name,
+        barWidth: 15,
+        data: c.data,
+        itemStyle: {
+          color:
+              {type: 'linear', x: 0, x2: 0, y: 0, y2: 1, colorStops: color[i]}
+        },
+      }, {
+        z: i + 10,
+        name: c.name,
+        type: 'pictorialBar',
+        symbolPosition: 'end',
+        symbol: 'circle',
+        symbolOffset: [0, '-50%'],
+        symbolSize: [15, 12.5],
+        data: diamondData[i],
+        itemStyle: {
+          color: color[i + 1] ? color[i + 1][0].color : null
+        },
+        tooltip: {show: false},
+      })
+
+      return p
+    }, [])
+
+
+// 最上边顶
+    dataArr.series.push({
+      name: dataArr.result[dataArr.result.length - 1].name,
+      z: 20,
+      type: "pictorialBar",
+      symbolPosition: "end",
+      data: diamondData[diamondData.length - 1],
+      symbol: "circle",
+      symbolOffset: ["0%", "-50%"],
+      symbolSize: [15, 12.5],
+      itemStyle: {
+        color: color[color.length - 1][0].color
+      },
+      tooltip: {show: false},
+    })
+
+// 最下边底
+    dataArr.series.push({
+      name: dataArr.result[0].name,
+      z: 30,
+      type: "pictorialBar",
+      symbolPosition: "start",
+      data: diamondData[0],
+      symbol: "circle",
+      symbolOffset: ["0%", "50%"],
+      symbolSize: [15, 12.5],
+      itemStyle: {
+        color: color[0][0].color
+      },
+      tooltip: {show: false},
+    })
+    setOption(dataArr);
+    loading.value = false;
+  } catch (e) {
+    console.log(e);
+    loading.value = false;
+  }
+};
+const setOption = (data: any) => {
+  option.value = {
+    tooltip: {trigger: "axis", backgroundColor: "rgba(0,0,0,0.7)", borderColor: "#000", borderWidth: 1, textStyle: {color: "#fff"}},
+    xAxis: {
+      axisTick: {show: true},
+      axisLine: {lineStyle: {color: 'rgba(255,255,255, .2)'}},
+      axisLabel: {fontSize: 12, color: '#fff'},
+      data: data.xData
+    },
+    yAxis: [{
+      name: '单位:件',
+      nameTextStyle: {color: '#fff', fontSize: 14},
+      nameGap: 25,
+      splitLine: {lineStyle: {color: 'rgba(255,255,255, .05)'}},
+      axisLine: {show: false,},
+      axisLabel: {fontSize: 12, color: '#fff'}
+    }],
+    grid: {top: '20%', left: '10%', right: '3%', bottom: '10%'},
+    legend: {
+      data: data.result.map(item => item.name),
+      textStyle: {fontSize: 12, color: '#fff'},
+      itemWidth: 15,
+      itemHeight: 10,
+      top: '10%',
+      right: '20'
+    },
+    series: data.series
+  };
+};
+onMounted(() => {
+  getData();
+});
+</script>
+<style scoped lang="scss">
+.left-center {
+  padding: 0 30px;
+
+  &-title {
+    font-size: 20px;
+    color: #fff;
+  }
+
+  .left-center-content {
+    height: calc(100% - 30px);
+  }
+}
+</style>

+ 240 - 0
src/views/judicial/left-top.vue

@@ -0,0 +1,240 @@
+<template>
+  <div class="left-top">
+    <div class="flex flex-wrap left-top-content-order" v-loading="loading">
+      <div class="flex left-top-content-item">
+        <img src="@/assets/img/home/done_order.png" alt="">
+        <div class="left-top-content-item-text">
+          <div class="left-top-content-item-text-title">行政执法工单</div>
+          <div class="left-top-content-item-text-num"><b>
+            <CountUp :endVal="state.order.completionCount" :duration="2"/>
+          </b> 件
+          </div>
+        </div>
+      </div>
+      <div class="flex left-top-content-item">
+        <img src="@/assets/img/home/wait_order.png" alt="">
+        <div class="left-top-content-item-text">
+          <div class="left-top-content-item-text-title">线索属实工单
+          </div>
+          <div class="left-top-content-item-text-num"><b>
+            <CountUp :endVal="state.order.haveToAcceptCount" :duration="2"/>
+          </b> 件
+          </div>
+        </div>
+      </div>
+      <div class="flex left-top-content-item">
+        <img src="@/assets/img/home/ex_order.png" alt="">
+        <div class="left-top-content-item-text">
+          <div class="left-top-content-item-text-title">线索不属实工单</div>
+          <div class="left-top-content-item-text-num"><b>
+            <CountUp :endVal="state.order.overTimeCount" :duration="2"/>
+          </b> 件
+          </div>
+        </div>
+      </div>
+      <div class="flex left-top-content-item">
+        <img src="@/assets/img/home/delay_order.png" alt="">
+        <div class="left-top-content-item-text">
+          <div class="left-top-content-item-text-title">推诿工单</div>
+          <div class="left-top-content-item-text-num"><b>
+            <CountUp :endVal="state.order.delayCount" :duration="2"/>
+          </b> 件
+          </div>
+        </div>
+      </div>
+      <div class="flex left-top-content-item">
+        <img src="@/assets/img/home/my_order.png" alt="">
+        <div class="left-top-content-item-text">
+          <div class="left-top-content-item-text-title">满意工单</div>
+          <div class="left-top-content-item-text-num"><b>
+            <CountUp :endVal="state.order.satisfiedCount" :duration="2"/>
+          </b> 件
+          </div>
+        </div>
+      </div>
+      <div class="flex left-top-content-item">
+        <img src="@/assets/img/home/provice_order.png" alt="">
+        <div class="left-top-content-item-text">
+          <div class="left-top-content-item-text-title">工单总量</div>
+          <div class="left-top-content-item-text-num"><b>
+            <CountUp :endVal="state.order.provinceOrderCount" :duration="2"/>
+          </b> 件
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import {onMounted, reactive, ref, watch} from "vue";
+import CountUp from "@/components/count-up";
+import {knowledge, workOrder} from "@/api/home";
+import dayjs from "dayjs";
+
+const props = defineProps({
+  dateArray: {
+    type: Array,
+    default: () => []
+  }
+})
+const loading = ref(false);
+const date = ref([]);
+watch(() => props.dateArray, (val: any) => {
+  date.value = val;
+}, {immediate: true})
+
+watch(() => props.dateArray, (val: any) => {
+  getWorkOrder();
+})
+const duration = ref(2);
+const state = reactive({
+  order: {
+    completionCount: 0,
+    completionRate: 0,
+    haveToAcceptCount: 0,
+    haveToAcceptRate: 0,
+    overTimeCount: 0,
+    overTimeRate: 0,
+    delayCount: 0,
+    delayRate: 0,
+    satisfiedCount: 0,
+    satisfiedRate: 0,
+    provinceOrderCount: 0,
+    provinceOrderCompletionCount: 0
+  },
+  knowledge: {
+    todayAddCount: 0,
+    thisMonthModifyCount: 0,
+    todayReadCount: 0,
+    knowledgeCount: 0
+  }
+});
+// 获取工单统计
+const getWorkOrder = async () => {
+  loading.value = true;
+  try {
+    const {result} = await workOrder({StartDate: dayjs(date.value[0]).format('YYYY-MM-DD'), EndDate: dayjs(date.value[1]).format('YYYY-MM-DD')});
+    state.order = result;
+    loading.value = false;
+  } catch (e) {
+    console.log(e);
+    loading.value = false;
+  }
+}
+// 获取知识库统计
+const getKnowledge = async () => {
+  loading.value = true;
+  try {
+    const {result} = await knowledge();
+    state.knowledge = result;
+    loading.value = false;
+  } catch (e) {
+    console.log(e);
+    loading.value = false;
+  }
+}
+
+onMounted(() => {
+  getWorkOrder();
+  getKnowledge();
+})
+</script>
+<style scoped lang="scss">
+.left-top {
+  padding: 0 30px;
+
+  .tab {
+    span {
+      cursor: pointer;
+      flex: 1;
+      height: 50px;
+      text-align: center;
+      line-height: 50px;
+      font-size: 16px;
+
+      margin-right: 20px;
+      background: url("@/assets/img/home/tab_bg.png") no-repeat;
+      background-size: 100% 100%;
+
+      &:last-child {
+        margin-right: 0;
+      }
+
+      &.active {
+        color: #fff;
+        font-size: 18px;
+        font-weight: bold;
+        position: relative;
+
+        &:after {
+          content: '';
+          display: block;
+          width: 50%;
+          height: 30px;
+          background: linear-gradient(to right, rgba(255, 255, 255, .2), rgba(255, 255, 255, .1));
+          border-radius: 20px;
+          position: absolute;
+          top: 20%;
+          left: 25%;
+        }
+      }
+    }
+  }
+
+  &-content {
+    &-order {
+      justify-content: space-between;
+      margin-top: 20px;
+    }
+
+    &-knowledge {
+      justify-content: space-between;
+      margin-top: 40px;
+    }
+
+    &-item {
+      width: 50%;
+      margin-bottom: 40px;
+
+      &:last-child {
+        margin-right: 0;
+        margin-bottom: 0;
+      }
+
+      img {
+        width: 50px;
+        height: 50px;
+      }
+
+      &-text {
+        margin-left: 10px;
+
+        &-title {
+          font-size: 16px;
+          color: #AAAFAF;
+          white-space: nowrap;
+        }
+
+        &-num {
+          color: #AAAFAF;
+
+          b {
+            font-size: 30px;
+            color: #fff;
+          }
+        }
+
+        &-rate {
+          font-size: 12px;
+          color: #fff;
+        }
+      }
+    }
+  }
+
+  .count-up-wrap {
+    display: inline-block;
+  }
+}
+
+</style>

+ 247 - 0
src/views/judicial/right-bottom.vue

@@ -0,0 +1,247 @@
+<template>
+  <div class="left_bottom">
+    <div class="left_bottom-title flex">
+      <div class="flex items-center">
+        <img src="@/assets/img/home/title_arrow.png" alt="">
+        占比分析
+      </div>
+      <div class="left_bottom-title-tabs">
+        <span :class="{active: activeIndex === 0}" class="mr-5 left_bottom-title-tabs-item" @click="change(0)">来源方式</span>
+        <span :class="{active: activeIndex === 1}" class="left_bottom-title-tabs-item" @click="change(1)">受理类型</span>
+      </div>
+    </div>
+    <div class="left_bottom-content">
+      <template v-if="dataList.length">
+        <v-chart class="chart" :option="option" :loading="loading" :loading-options="loadingOptions"/>
+      </template>
+      <template v-else>
+        <EmptyCom></EmptyCom>
+      </template>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import {ref, onMounted, watch, computed} from "vue";
+import {proportionAnalysis} from "api/home";
+import dayjs from "dayjs";
+import EmptyCom from "@/components/empty-com";
+import {loadingOptions} from "@/utils/constants";
+
+const props = defineProps({
+  dateArray: {
+    type: Array,
+    default: () => []
+  }
+})
+const date = ref([]);
+watch(() => props.dateArray, (val: any) => {
+  date.value = val;
+}, {immediate: true})
+
+watch(() => props.dateArray, (val: any) => {
+  getData();
+})
+
+const activeIndex = ref(0);
+const option = ref<any>({});
+const activeText = computed(() => {
+  return activeIndex.value === 0 ? '来源占比' : '类型占比';
+});
+const change = (index: number) => {
+  activeIndex.value = index;
+  getData();
+};
+const dataList = ref([])
+const loading = ref(false);
+const getData = async () => {
+  loading.value = true;
+  try {
+    const {result} = await proportionAnalysis({
+      StartDate: dayjs(date.value[0]).format('YYYY-MM-DD'),
+      EndDate: dayjs(date.value[1]).format('YYYY-MM-DD'),
+      IsSource: activeIndex.value === 0
+    });
+    const legendDate = result.map((item: any) => item.name).filter((item: any) => item);
+    dataList.value = result.map((item: any) => {
+      return {
+        name: item.name ?? '',
+        value: item.hasCount,
+        ...item
+      }
+    }).filter((item: any) => item.name)
+    setOption(legendDate, dataList.value);
+    loading.value = false;
+  } catch (e) {
+    loading.value = false;
+    console.log(e);
+  }
+};
+const setOption = (legendDate: any, data: any) => {
+  option.value =
+      {
+        tooltip: {
+          trigger: 'item'
+        },
+        legend: {
+          type: 'scroll',
+          pageIconColor: '#fff',
+          pageIconInactiveColor: '#333',
+          height: '180',
+          icon: 'circle',
+          top: 'center',
+          right: '0',
+          orient: "vertical",
+          itemGap: 20,
+          data: legendDate,
+          textStyle: {
+            color: '#fff',
+            fontSize: 14,
+          },
+          formatter: name => {
+            const item = dataList.value.find(item => item.name == name)
+            const len = name.length;
+            let str = ''
+            if (len > 5) {
+              name = name.slice(0, 8) + '...';
+            } else {
+              name = name + str;
+            }
+            return `${name}  ${item.hasRate}% (${item.hasCount}件)`;  //返回出图例所显示的内容是名称+百分比
+          }
+        },
+        graphic: [
+          {
+            type: 'text',
+            z: 100,
+            left: '23%',
+            top: '40%',
+            style: {
+              fill: '#fff',
+              text: '100%',
+              font: '28px Microsoft YaHei',
+            }
+          }, {
+            type: 'text',
+            z: 100,
+            left: '24%',
+            top: '56%',
+            style: {
+              fill: '#fff',
+              text: activeText.value,
+              font: '16px Microsoft YaHei'
+            }
+          }],
+        series: [
+          {
+            name: '',
+            type: 'gauge',
+            splitNumber: 10,
+            radius: '65%',
+            center: ['30%', '50%'],
+            startAngle: 0,
+            endAngle: 360,
+            axisLine: {
+              show: false
+            },
+            axisTick: {
+              show: true,
+              lineStyle: {
+                width: 4,
+              },
+              length: 8,
+              splitNumber: 3
+            },
+            splitLine: {
+              show: false
+            },
+            axisLabel: {
+              show: false
+            },
+            detail: {
+              show: false
+            }
+          },
+          {
+            name: "数量",//内部圆
+            type: "pie",
+            animation: false,
+            clockwise: false,
+            radius: "38%",
+            center: ["30%", "50%"],
+            data: [10],
+            itemStyle: {
+              color: '#082141',
+            },
+            label: {
+              show: false,
+
+            },
+            emphasis: {
+              show: false,
+              textStyle: {
+                fontSize: "14",
+              },
+            },
+            labelLine: {
+              show: false,
+            },
+          },
+          {
+            name: '',
+            type: 'pie',
+            radius: ['55%', '80%'],
+            center: ['30%', '50%'],
+            avoidLabelOverlap: false,
+            itemStyle: {
+              borderRadius: 0,
+              borderColor: '#050D0E',
+              borderWidth: 10,
+            },
+            label: {
+              show: false,
+              formatter: '{b}:{d}%', // 用来换行
+            },
+            labelLine: {
+              show: false
+            },
+            data: data
+          }
+        ]
+      };
+};
+onMounted(() => {
+  getData();
+});
+</script>
+<style scoped lang="scss">
+.left_bottom {
+  padding: 0 30px;
+
+  &-title {
+    font-size: 20px;
+    color: #fff;
+    justify-content: space-between;
+    align-items: center;
+
+    &-tabs {
+      font-size: 14px;
+      color: #7DBDEC;
+
+      &-item {
+        padding: 5px 10px;
+        cursor: pointer;
+      }
+    }
+
+    .active {
+      color: #B4D7EC;
+      border: 1px solid #B4D7EC;
+      border-radius: 5px;
+    }
+  }
+
+  &-content {
+    height: calc(100% - 40px);
+  }
+}
+</style>

+ 284 - 0
src/views/judicial/right-center.vue

@@ -0,0 +1,284 @@
+<template>
+  <div class="right_center">
+    <div class="right_center-title flex">
+      <div class="flex items-center">
+        <img src="@/assets/img/home/title_arrow.png" alt="">
+        工单月份趋势图
+      </div>
+      <el-date-picker
+          v-model="yearValue"
+          type="year"
+          placeholder="选择年份"
+          :clearable="false"
+          value-format="YYYY"
+          @change="changeYear"
+      />
+    </div>
+    <div class="right_center-content">
+      <template v-if="list.length">
+        <v-chart class="chart" :option="option" :loading="loading" :loading-options="loadingOptions"/>
+      </template>
+      <template v-else>
+        <EmptyCom></EmptyCom>
+      </template>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import {onMounted, ref} from "vue";
+import dayjs from "dayjs";
+import EmptyCom from "@/components/empty-com";
+import {loadingOptions} from "@/utils/constants";
+import {graphic} from "echarts/core";
+
+const date = ref([]);
+const yearValue = ref(dayjs().year().toString());
+console.log(yearValue.value)
+const list = ref<any>([]);
+const loading = ref(false);
+const option = ref<any>({});
+const changeYear = () => {
+  getData();
+}
+const getData = async () => {
+  loading.value = true;
+  try {
+    list.value = [{}]
+    const xData = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
+    const data = [20, 19, 20, 23, 25, 29, 27, 28, 29, 26, 24, 23, 25]
+    setOption(xData, data);
+    loading.value = false;
+  } catch (e) {
+    loading.value = false;
+    console.log(e);
+  }
+};
+const setOption = (xData: any, data: any) => {
+  console.log(xData, data)
+  option.value = {
+    tooltip: {
+      show: true,
+      trigger: "axis",
+      formatter: (params) => {
+        let arr = [...params];
+        let str = '';
+        arr.forEach((item, index) => {
+          str += item.marker + item.seriesName + '  ' + item.data + '<br />';
+        });
+        str = arr[0].name + '<br />' + str;
+        return str;
+      },
+    },
+    grid: {
+      top: "15%",
+      left: "8%",
+      right: "5%",
+      bottom: "18%",
+      containLabel: true,
+    },
+    xAxis: [
+      {
+        type: "category",
+        axisLine: {
+          onZero: true,
+          lineStyle: {
+            color: "#81b0d0",
+          },
+        },
+        axisLabel: {
+          interval: 0,
+          align: "center",
+          margin: 10,
+          color: "#fff",
+          rotate: 0,
+        },
+        splitLine: {
+          show: false,
+        },
+        axisTick: {
+          show: false,
+        },
+        boundaryGap: false,
+        data: xData,
+      },
+    ],
+    yAxis: [
+      {
+        type: "value",
+        splitLine: {
+          show: true,
+          lineStyle: {
+            color: "rgba(221, 228, 241,.3)",
+          },
+        },
+        axisLabel: {},
+        axisTick: {
+          show: false,
+        },
+      },
+    ],
+    series: [
+      {
+        data: data,
+        type: 'line',
+        smooth: true,
+
+        symbolSize: 0.01,
+
+        areaStyle: {
+          color: new graphic.LinearGradient(0, 0, 0, 0.9, [
+            {
+              offset: 0,
+              // color: 'rgba(58,77,233,0.8)'
+              color: 'rgba(170, 201 ,234,0.8)'
+            },
+            {
+              offset: 1,
+              // color: 'rgba(58,77,233,0)'
+              color: 'rgba(170, 201 ,234,0)'
+            },
+            {
+              offset: 0,
+              color: 'rgba(170, 201 ,234,1)'
+              // color: 'rgba(58,77,233,0.9)'
+            },
+          ])
+        },
+        itemStyle: {
+          // color: "#3D5DF4",
+          color: '#4EA6FE'
+        },
+        lineStyle: {
+          // 线条加阴影
+          // 设置阴影颜色
+          // shadowColor: "#3D5DF4",
+          shadowColor: "#4EA6FE",
+          shadowOffsetX: 0,
+          // 设置阴影沿y轴偏移量为9
+          shadowOffsetY: 9,
+          // 设置阴影的模糊大小
+          shadowBlur: 10,
+          // 设置线条渐变色
+          color: new graphic.LinearGradient(0, 0, 1, 0, [
+            {offset: 0, color: "#94C2FD00"},
+            // { offset: 0.2, color: "#3D5DF4" },
+            {offset: 0.2, color: "#4EA6FE"},
+            {offset: 0.1, color: "#5B8FF900"},
+          ]),
+        },
+
+        emphasis: {
+          scale: 1000,
+          itemStyle: {
+            color: "#3D5DF4",
+            borderColor: "#FFFFFF",
+            borderWidth: 3,
+            borderType: "solid",
+          },
+        }
+      }
+    ],
+  }
+};
+onMounted(() => {
+  getData();
+});
+</script>
+<style scoped lang="scss">
+.right_center {
+  padding: 0 30px;
+  font-size: 15px;
+
+  &-title {
+    font-size: 20px;
+    color: #fff;
+    justify-content: space-between;
+    align-items: center;
+  }
+
+  &-content {
+    margin-top: 15px;
+    height: 100%;
+
+    .scroll {
+      height: 300px;
+      overflow: hidden;
+
+      &-item {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: 8px 5px;
+        color: #D2D4D5;
+        margin-bottom: 15px;
+
+        &:nth-child(odd) {
+          background-color: rgba(255, 255, 255, 0.1);
+          border-radius: 5px;
+        }
+
+
+        &-area {
+          display: inline-block;
+          background-color: #69BBF6;
+          padding: 0 6px;
+          color: #333;
+          border-top-left-radius: 4px;
+          border-bottom-left-radius: 4px;
+          white-space: nowrap;
+          position: relative;
+
+          &:after {
+            content: "";
+            border-left: 8px solid #69BBF6;
+            border-right: 12px solid transparent;
+            border-top: 12px solid transparent;
+            border-bottom: 11px solid transparent;
+            width: 0;
+            height: 0;
+            position: absolute;
+            right: -20px;
+          }
+        }
+
+        .three {
+          background-color: #ECA455;
+          position: relative;
+        }
+
+        .three:after {
+          content: "";
+          border-left: 8px solid #ECA455;
+          border-right: 12px solid transparent;
+          border-top: 12px solid transparent;
+          border-bottom: 11px solid transparent;
+          width: 0;
+          height: 0;
+          position: absolute;
+          right: -20px;
+        }
+
+        &-title {
+          width: 50%;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+          margin-left: 20px;
+        }
+
+        &-hotspot {
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+          flex: 1;
+          margin-left: 30px;
+        }
+      }
+    }
+  }
+}
+
+.count-up-wrap {
+  display: inline-block;
+}
+</style>

+ 167 - 0
src/views/judicial/right-top.vue

@@ -0,0 +1,167 @@
+<template>
+  <div class="right_center">
+    <div class="right_center-title flex">
+      <div class="flex items-center">
+        <img src="@/assets/img/home/title_arrow.png" alt="">
+        部门满意度排行榜
+      </div>
+    </div>
+    <div class="right_center-content" v-loading="loading">
+      <template v-if="list.length">
+        <div class="scroll">
+          <div class="scroll-item" v-for="(item, index) in list" :key="index">
+            <span class="scroll-item-area" :class="index<=2 ? 'three' : ''">NO.{{ index + 1 }}</span>
+            <el-tooltip placement="top">
+              <template #content> {{ item.visitOrgName }}</template>
+              <span class="scroll-item-title">{{ item.visitOrgName }}</span>
+            </el-tooltip>
+            <span class="scroll-item-hotspot"><CountUp :endVal="item.visitCount " :duration="2"/>(件)</span>
+            <span class="scroll-item-num">{{ item.satisfiedRate }}%</span>
+          </div>
+        </div>
+      </template>
+      <template v-else>
+        <EmptyCom></EmptyCom>
+      </template>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import {onMounted, ref, watch} from "vue";
+import CountUp from "@/components/count-up";
+import {departmentSatisfaction} from "api/home";
+import dayjs from "dayjs";
+import EmptyCom from "@/components/empty-com";
+
+const props = defineProps({
+  dateArray: {
+    type: Array,
+    default: () => []
+  }
+})
+const date = ref([]);
+watch(() => props.dateArray, (val: any) => {
+  date.value = val;
+}, {immediate: true})
+
+watch(() => props.dateArray, (val: any) => {
+  getData();
+})
+const list = ref<any>([]);
+const loading = ref(false);
+const getData = async () => {
+  loading.value = true;
+  try {
+    const {result} = await departmentSatisfaction({
+      StartDate: dayjs(date.value[0]).format('YYYY-MM-DD'),
+      EndDate: dayjs(date.value[1]).format('YYYY-MM-DD'),
+    });
+    list.value = result;
+    loading.value = false;
+  } catch (e) {
+    console.log(e);
+    loading.value = false;
+  }
+}
+onMounted(() => {
+  getData();
+});
+</script>
+<style scoped lang="scss">
+.right_center {
+  padding: 0 30px;
+  font-size: 15px;
+
+  &-title {
+    font-size: 20px;
+    color: #fff;
+    justify-content: space-between;
+    align-items: center;
+  }
+
+  &-content {
+    margin-top: 15px;
+    height: 100%;
+
+    .scroll {
+      height: 300px;
+      overflow: hidden;
+
+      &-item {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: 8px 5px;
+        color: #D2D4D5;
+        margin-bottom: 15px;
+
+        &:nth-child(odd) {
+          background-color: rgba(255, 255, 255, 0.1);
+          border-radius: 5px;
+        }
+
+
+        &-area {
+          display: inline-block;
+          background-color: #69BBF6;
+          padding: 0 6px;
+          color: #333;
+          border-top-left-radius: 4px;
+          border-bottom-left-radius: 4px;
+          white-space: nowrap;
+          position: relative;
+
+          &:after {
+            content: "";
+            border-left: 8px solid #69BBF6;
+            border-right: 12px solid transparent;
+            border-top: 12px solid transparent;
+            border-bottom: 11px solid transparent;
+            width: 0;
+            height: 0;
+            position: absolute;
+            right: -20px;
+          }
+        }
+
+        .three {
+          background-color: #ECA455;
+          position: relative;
+        }
+
+        .three:after {
+          content: "";
+          border-left: 8px solid #ECA455;
+          border-right: 12px solid transparent;
+          border-top: 12px solid transparent;
+          border-bottom: 11px solid transparent;
+          width: 0;
+          height: 0;
+          position: absolute;
+          right: -20px;
+        }
+
+        &-title {
+          width: 50%;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+          margin-left: 20px;
+        }
+
+        &-hotspot {
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+          flex: 1;
+          margin-left: 30px;
+        }
+      }
+    }
+  }
+}
+
+.count-up-wrap {
+  display: inline-block;
+}
+</style>