58c0f11fe2f23a1be2dec768f9ac02107301a634..32eedf2857255cf29985ffc0cc73e75eccda39bf
2025-09-20 Riku
2025.9.20 完成现场巡查基础数据产品和月度巡查简报的中间数据产品
32eedf 对比 | 目录
2025-09-20 riku
2025.9.20 数据产品(待完成)
0796ee 对比 | 目录
已修改11个文件
449 ■■■■ 文件已修改
src/api/index.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components.d.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/search-option/FYOptionTopTask.vue 41 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/echart-util.js 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/components/BaseProdProcess.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/components/ProdDownload.vue 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/components/ProdQueryOptCompare.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/middle-data-product/ManageMiddleProd.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/middle-data-product/ProdEvaluationSummary.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/middle-data-product/ProdProblemCountSummary.vue 166 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/middle-data-product/ProdProblemTypeSummary.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/index.js
@@ -13,8 +13,8 @@
let ip2_file = 'https://fyami.com.cn/';
if (debug) {
  ip1 = 'http://192.168.0.103:9001/';
  // ip1 = 'http://localhost:9001/';
  // ip1 = 'http://192.168.0.103:9001/';
  ip1 = 'http://localhost:9001/';
  // ip1_file = 'http://192.168.0.138:8080/';
  // ip2 = 'http://192.168.0.138:8080/';
  // ip2_file = 'https://fyami.com.cn/';
src/components.d.ts
@@ -13,19 +13,16 @@
    CompGenericWrapper: typeof import('./components/CompGenericWrapper.vue')['default']
    CompQuickSet: typeof import('./components/search-option/CompQuickSet.vue')['default']
    Content: typeof import('./components/core/Content.vue')['default']
    copy: typeof import('./components/search-option/FYOptionScene copy.vue')['default']
    ElAffix: typeof import('element-plus/es')['ElAffix']
    ElAlert: typeof import('element-plus/es')['ElAlert']
    ElAside: typeof import('element-plus/es')['ElAside']
    ElAvatar: typeof import('element-plus/es')['ElAvatar']
    ElBadge: typeof import('element-plus/es')['ElBadge']
    ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
    ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
    ElButton: typeof import('element-plus/es')['ElButton']
    ElCalendar: typeof import('element-plus/es')['ElCalendar']
    ElCard: typeof import('element-plus/es')['ElCard']
    ElCascader: typeof import('element-plus/es')['ElCascader']
    ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
    ElCol: typeof import('element-plus/es')['ElCol']
    ElCollapse: typeof import('element-plus/es')['ElCollapse']
    ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
@@ -46,21 +43,16 @@
    ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
    ElInput: typeof import('element-plus/es')['ElInput']
    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
    ElLink: typeof import('element-plus/es')['ElLink']
    ElMain: typeof import('element-plus/es')['ElMain']
    ElMenu: typeof import('element-plus/es')['ElMenu']
    ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
    ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
    ElOption: typeof import('element-plus/es')['ElOption']
    ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
    ElPagination: typeof import('element-plus/es')['ElPagination']
    ElPopover: typeof import('element-plus/es')['ElPopover']
    ElRadio: typeof import('element-plus/es')['ElRadio']
    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
    ElRow: typeof import('element-plus/es')['ElRow']
    ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
    ElSegmented: typeof import('element-plus/es')['ElSegmented']
    ElSelect: typeof import('element-plus/es')['ElSelect']
    ElSpace: typeof import('element-plus/es')['ElSpace']
    ElStep: typeof import('element-plus/es')['ElStep']
@@ -74,7 +66,6 @@
    ElTag: typeof import('element-plus/es')['ElTag']
    ElText: typeof import('element-plus/es')['ElText']
    ElTooltip: typeof import('element-plus/es')['ElTooltip']
    ElTransfer: typeof import('element-plus/es')['ElTransfer']
    ElTree: typeof import('element-plus/es')['ElTree']
    ElUpload: typeof import('element-plus/es')['ElUpload']
    Footer: typeof import('./components/core/Footer.vue')['default']
src/components/search-option/FYOptionTopTask.vue
@@ -3,11 +3,11 @@
    <el-select
      :model-value="formatedValue"
      @update:model-value="handleChange"
      placeholder="总任务"
      :placeholder="label"
      style="width: 260px"
    >
      <el-option
        v-for="s in topTasks"
        v-for="s in filtedBeforeTask"
        :key="s.value"
        :label="s.label"
        :value="s.value"
@@ -37,17 +37,44 @@
      type: String,
      default: 'topTaskId'
    },
    // 选项筛选条件,筛选某任务之前的相同行政区划内的任务
    beforeTask: {
      type: Object,
      default: () => {
        return {};
      }
    }
  },
  emits: ['update:value'],
  data() {
    return {
      selected: {},
      topTasks: [],
      topTasks: []
    };
  },
  computed: {
    // 选择框中使用顶层任务id作为选项值
    formatedValue() {
      return this.value.tguid;
    },
    // 某任务之前的相同行政区划内的任务
    filtedBeforeTask() {
      const filteredTasks = this.topTasks.filter((t) => {
        return (
          (!this.beforeTask.provincecode ||
            this.beforeTask.provincecode == t.data.provincecode) &&
          (!this.beforeTask.citycode ||
            this.beforeTask.citycode == t.data.citycode) &&
          (!this.beforeTask.districtcode ||
            this.beforeTask.districtcode == t.data.districtcode) &&
          (!this.beforeTask.starttime ||
            t.data.starttime < this.beforeTask.starttime)
        );
      });
      if (filteredTasks.length > 0) {
        this.handleChange(filteredTasks[0]?.value);
      }
      return filteredTasks;
    }
  },
  methods: {
@@ -69,13 +96,11 @@
    },
    //查询子任务统计信息
    handleChange(value) {
      const task = this.topTasks.find(
        (t) => t.data.tguid == value
      );
      const param = task ? task.data : {}
      const task = this.topTasks.find((t) => t.data.tguid == value);
      const param = task ? task.data : {};
      this.$emit('update:value', param);
    },
    }
  },
  mounted() {
    this.getOptions();
src/utils/echart-util.js
@@ -69,6 +69,79 @@
  };
}
function barChartOption() {
  return {
    title: {
      text: `柱状图默认名称`,
      left: 'center' // 标题居中显示
    },
    // 添加工具栏配置,包含下载功能
    toolbox: {
      show: true,
      feature: {
        saveAsImage: {
          show: true,
          title: '下载图表',
          type: 'png',
          pixelRatio: 2 // 提高图片清晰度
        }
      }
    },
    tooltip: {
      trigger: 'axis', // 柱状图使用axis触发tooltip
      axisPointer: {
        type: 'shadow' // 显示阴影指示器
      },
      formatter: '{b}: {c}' // 显示格式:名称: 数量
    },
    legend: {
      show: true,
      orient: 'horizontal',
      bottom: '0%', // 图例底部水平排列
    },
    grid: {
      // left: '3%',
      // right: '4%',
      bottom: '10%',
      top: '15%',
      containLabel: true
    },
    xAxis: {
      name: '坐标轴',
      type: 'category',
      data: ['sample1', 'sample2', 'sample3'], // X轴数据
      axisTick: {
        alignWithLabel: true
      },
      axisLabel: {
        rotate: 45,
      }
    },
    yAxis: {
      type: 'value',
      name: '数量', // Y轴名称
      axisLine: {
        show: true
      },
      axisLabel: {
        formatter: '{value}'
      }
    },
    series: [
      {
        name: 'sample',
        type: 'bar', // 图表类型改为柱状图
        data: [100, 200, 300], // 数据值
        label: {
          show: true,
          position: 'top', // 标签显示在柱子顶部
          formatter: '{c}' // 标签格式:数量
        }
      }
    ]
  };
}
// 通过 ECharts API 下载图片的函数
function downloadChartImage(chart, fileName) {
  if (!chart) return; // 确保图表已初始化
@@ -92,4 +165,4 @@
  document.body.removeChild(link);
}
export { pieChartOption, downloadChartImage };
export { pieChartOption, barChartOption, downloadChartImage };
src/views/fysp/data-product/components/BaseProdProcess.vue
@@ -8,7 +8,7 @@
      >
        <div v-show="showStep1Content">
          <template v-if="$slots.step1">
            <slot name="step1"></slot>
            <slot name="step1" :onSearch="onSearch"></slot>
          </template>
          <template v-else>
            <ProdQueryOpt :loading="loading" @submit="onSearch"> </ProdQueryOpt>
src/views/fysp/data-product/components/ProdDownload.vue
@@ -5,13 +5,13 @@
    </template>
    <el-form :inline="false" label-position="left" label-width="150px">
      <el-form-item label="区县">
        <el-text>{{ queryOpt.districtName }}</el-text>
        <el-text>{{ opts.districtName }}</el-text>
      </el-form-item>
      <el-form-item label="时间范围">
        <el-text>{{ queryOpt.startTime }} 至 {{ queryOpt.endTime }}</el-text>
        <el-text>{{ opts.startTime }} 至 {{ opts.endTime }}</el-text>
      </el-form-item>
      <el-form-item label="场景类型">
        <el-text>{{ queryOpt.sceneTypeName }}</el-text>
        <el-text>{{ opts.sceneTypeName }}</el-text>
      </el-form-item>
      <el-form-item label="产品形式">
        <el-radio-group v-model="downloadType">
@@ -53,6 +53,13 @@
const emit = defineEmits(['submit']);
const downloadType = ref('1');
const opts = computed(() => {
  if (props.queryOpt instanceof Array && props.queryOpt.length > 0) {
    return props.queryOpt[0];
  } else {
    return props.queryOpt;
  }
});
const submit = () => {
  emit('submit', {
src/views/fysp/data-product/components/ProdQueryOptCompare.vue
@@ -3,17 +3,31 @@
    <template #header>
      <div><el-text tag="b" size="large">产品生成选项</el-text></div>
    </template>
    <el-form :inline="true" :model="formSearch">
      <FYOptionTopTask v-model:value="formSearch.topTask"></FYOptionTopTask>
      <FYOptionScene
        :allOption="false"
        :type="2"
        v-model:value="formSearch.scenetype"
      ></FYOptionScene>
    </el-form>
    <el-form :inline="true" :model="formSearch2">
      <FYOptionTopTask v-model:value="formSearch2.topTask"></FYOptionTopTask>
    </el-form>
    <el-space fill>
      <el-alert type="info" show-icon :closable="false">
        <p>选择本期需要统计的总任务和场景类型</p>
      </el-alert>
      <el-form :inline="true" :model="formSearch">
        <FYOptionTopTask v-model:value="formSearch.topTask"></FYOptionTopTask>
        <FYOptionScene
          :allOption="false"
          :type="2"
          v-model:value="formSearch.scenetype"
        ></FYOptionScene>
      </el-form>
    </el-space>
    <el-space fill>
      <el-alert type="info" show-icon :closable="false">
        <p>选择需要进行对比的历史版本</p>
      </el-alert>
      <el-form :inline="true" :model="formSearch2">
        <FYOptionTopTask
          :beforeTask="formSearch.topTask"
          v-model:value="formSearch2.topTask"
        ></FYOptionTopTask>
      </el-form>
    </el-space>
    <template #footer>
      <el-row v-show="active" justify="end">
        <el-button
src/views/fysp/data-product/middle-data-product/ManageMiddleProd.vue
@@ -18,10 +18,10 @@
    name: '分街镇单场景问题数均值',
    path: 'problemCountSummary'
  },
  {
    name: '监测设备汇总',
    path: 'monitorDeviceSummary'
  },
  // {
  //   name: '监测设备汇总',
  //   path: 'monitorDeviceSummary'
  // },
  {
    name: '评估情况汇总',
    path: 'evaluationSummary'
src/views/fysp/data-product/middle-data-product/ProdEvaluationSummary.vue
@@ -6,6 +6,57 @@
    @onStep3="onStep3"
    :loading="loading"
  >
    <template #step2="{ contentHeight }">
      <el-table
        id="prod-evaluation-summary-table"
        :data="tableData"
        v-loading="loading"
        :height="contentHeight + 'px'"
        table-layout="fixed"
        :show-overflow-tooltip="true"
        size="small"
        border
      >
        <el-table-column fixed="left" prop="index" label="排名" width="50">
        </el-table-column>
        <el-table-column prop="townName" label="街镇" min-width="110" />
        <el-table-column
          prop="validSceneCount"
          label="建设中工地数"
          min-width="90"
        >
        </el-table-column>
        <el-table-column
          prop="evaluationCount"
          label="评估点次"
          min-width="50"
        />
        <el-table-column label="防治规范性点次评估" min-width="60">
          <el-table-column prop="evalLevelACount" label="规范" min-width="50" />
          <el-table-column
            prop="evalLevelBCount"
            label="基本规范"
            min-width="50"
          />
          <el-table-column
            prop="evalLevelCCount"
            label="不规范"
            min-width="50"
          />
          <el-table-column
            prop="evalLevelDCount"
            label="严重不规范"
            min-width="50"
          />
        </el-table-column>
        <el-table-column
          prop="evalLevelRatioAB"
          label="规范及基本规范评估占比"
          min-width="90"
          :formatter="ratioFormat"
        />
      </el-table>
    </template>
  </BaseProdProcess>
</template>
<script setup>
@@ -17,12 +68,48 @@
const { active, changeActive } = useProdStepChange();
const loading = ref(false);
const tableData = ref([]);
function onStep1(opt) {}
function onStep1(opt) {
  loading.value = true;
  dataprodmiddleApi
    .fetchEvaluationByArea(opt)
    .then((res) => {
      if (res.success) {
        tableData.value = res.data
          .sort((a, b) => {
            return b.evalLevelRatioAB - a.evalLevelRatioAB;
          })
          .map((item, index) => {
            return {
              ...item,
              index: index + 1
            };
          });
      }
      changeActive();
    })
    .finally(() => {
      loading.value = false;
    });
}
function onStep2() {
  changeActive();
}
function onStep3(val) {}
function onStep3(val) {
  if (val.downloadType == '1') {
    loading.value = true;
    conversionFromTable(
      'prod-evaluation-summary-table',
      '扬尘污染问题类型占比清单'
    );
    loading.value = false;
  }
}
function ratioFormat(row, column, cellValue, index) {
  return Math.round(cellValue * 1000) / 10 + '%';
}
</script>
src/views/fysp/data-product/middle-data-product/ProdProblemCountSummary.vue
@@ -1,33 +1,189 @@
<template>
  <BaseProdProcess
    v-model:active="active"
    @onStep1="onStep1"
    @onStep2="onStep2"
    @onStep3="onStep3"
    :loading="loading"
  >
    <template #step1>
      <ProdQueryOptCompare @submit="onStep1"></ProdQueryOptCompare>
    <template #step1="{ onSearch }">
      <ProdQueryOptCompare :loading="loading" @submit="onSearch"></ProdQueryOptCompare>
    </template>
    <template #step2="{ contentHeight }">
      <el-scrollbar :height="contentHeight">
        <el-table
          id="prod-problem-count-table"
          :data="tableData"
          v-loading="loading"
          table-layout="fixed"
          :show-overflow-tooltip="true"
          size="small"
          border
        >
          <el-table-column fixed="left" type="index" label="编号" width="50">
          </el-table-column>
          <el-table-column
            fixed="left"
            prop="districtName"
            label="区县"
            width="110"
          >
          </el-table-column>
          <el-table-column prop="townName" label="街镇" width="110" />
          <el-table-column
            prop="sceneCount"
            label="本期场景数"
            min-width="70"
          />
          <el-table-column
            prop="lastSceneCount"
            label="上期场景数"
            min-width="70"
          />
          <el-table-column
            prop="problemCount"
            label="本期问题数"
            min-width="70"
          />
          <el-table-column
            prop="lastProblemCount"
            label="上期问题数"
            min-width="70"
          />
          <el-table-column
            prop="ratio"
            label="本期问题数均值"
            min-width="70"
            :formatter="ratioFormat"
          />
          <el-table-column
            prop="lastRatio"
            label="上期问题数均值"
            min-width="70"
            :formatter="ratioFormat"
          />
        </el-table>
        <el-row justify="center">
          <div
            ref="chartRef"
            style="height: 400px; width: 100%; max-width: 800px"
          ></div>
        </el-row>
      </el-scrollbar>
    </template>
  </BaseProdProcess>
</template>
<script setup>
import { ref } from 'vue';
import { computed, ref } from 'vue';
import * as echarts from 'echarts';
import dayjs from 'dayjs';
import BaseProdProcess from '@/views/fysp/data-product/components/BaseProdProcess.vue';
import dataprodmiddleApi from '@/api/fysp/dataprodmiddleApi.js';
import { conversionFromTable } from '@/utils/excel';
import { useProdStepChange } from '@/views/fysp/data-product/prod-step-change.js';
import { barChartOption, downloadChartImage } from '@/utils/echart-util.js';
import ProdQueryOptCompare from '@/views/fysp/data-product/components/ProdQueryOptCompare.vue';
const { active, changeActive } = useProdStepChange();
const loading = ref(false);
const tableData1 = ref([]);
const tableData2 = ref([]);
const chartRef = ref(null);
let chart;
const tableData = computed(() => {
  return tableData1.value.map((item) => {
    const last = tableData2.value.find(
      (item2) => item2.townCode === item.townCode
    );
    item.ratio = Math.round(item.ratio * 10) / 10 || 0;
    return {
      ...item,
      lastSceneCount: last?.sceneCount || 0,
      lastProblemCount: last?.problemCount || 0,
      lastRatio: Math.round(item.ratio * 10) / 10 || 0
    };
  });
});
function onStep1(opts) {
  console.log('onStep1', opts);
  loading.value = true;
  const p1 = dataprodmiddleApi.fetchProblemCountByArea(opts[0]).then((res) => {
    if (res.success) {
      tableData1.value = res.data;
    }
  });
  const p2 = dataprodmiddleApi.fetchProblemCountByArea(opts[1]).then((res) => {
    if (res.success) {
      tableData2.value = res.data;
    }
  });
  Promise.all([p1, p2])
    .then(() => {
      changeActive();
      setTimeout(() => {
        genChart(opts[0], opts[1]);
      }, 500);
    })
    .finally(() => {
      loading.value = false;
    });
}
function onStep2() {
  changeActive();
}
function onStep3(val) {}
function onStep3(val) {
  if (val.downloadType == '1') {
    loading.value = true;
    conversionFromTable('prod-problem-count-table', '扬尘污染问题数均值对比');
    downloadChartImage(chart, '扬尘污染问题数均值对比');
    loading.value = false;
  }
}
function genChart(opt1, opt2) {
  if (chart == undefined) {
    chart = echarts.init(chartRef.value);
  }
  const year = dayjs(opt1.startTime).year();
  const month1 = dayjs(opt1.startTime).month() + 1;
  const month2 = dayjs(opt2.startTime).month() + 1;
  const time = `${year}年${month1}月、${month2}月`;
  const option = barChartOption();
  option.title.text = `${time}各街道(镇)${opt1.sceneTypeName}扬尘污染问题数均值对比`;
  option.xAxis.name = '街道(镇)';
  option.xAxis.data = tableData.value.map((item) => item.townName);
  option.yAxis.name = '问题数均值';
  option.series = [
    {
      name: `${month1}月`,
      type: 'bar', // 图表类型改为柱状图
      data: tableData1.value.map((item) => item.ratio),
      label: {
        show: true,
        position: 'top', // 标签显示在柱子顶部
        formatter: '{c}' // 标签格式:数量
      }
    },
    {
      name: `${month2}月`,
      type: 'bar', // 图表类型改为柱状图
      data: tableData1.value.map((item) => item.ratio),
      label: {
        show: true,
        position: 'top', // 标签显示在柱子顶部
        formatter: '{c}' // 标签格式:数量
      }
    }
  ];
  chart.setOption(option);
}
function ratioFormat(row, column, cellValue, index) {
  return Math.round(cellValue * 10) / 10;
}
</script>
src/views/fysp/data-product/middle-data-product/ProdProblemTypeSummary.vue
@@ -99,7 +99,7 @@
function onStep3(val) {
  if (val.downloadType == '1') {
    loading.value = true;
    // conversionFromTable('prod-problem-type-table', '扬尘污染问题类型占比清单');
    conversionFromTable('prod-problem-type-table', '扬尘污染问题类型占比清单');
    downloadChartImage(chart, '扬尘污染问题类型占比');
    loading.value = false;
  }