Browse Source

新增招商

littleblue55 4 weeks ago
parent
commit
5140750738
1 changed files with 637 additions and 0 deletions
  1. 637 0
      src/views/investment/index.vue

+ 637 - 0
src/views/investment/index.vue

@@ -0,0 +1,637 @@
+<template>
+  <!-- 招商 - littlegreen - 补充form和label -->
+  <div
+    :class="className"
+    :style="{ height: '100%', width: width, padding: '20px' }"
+  >
+    <div class="select-container">
+      <el-form
+        :model="form"
+        :rules="rules"
+        ref="ruleForm"
+        style="
+          display: grid;
+          grid-template-columns: 1fr 1fr;
+          align-items: center;
+          grid-gap: 20px;
+        "
+      >
+        <el-form-item
+          label="新增企业月均工业产值"
+          label-width="200px"
+          prop="newMonthValue"
+        >
+          <div style="display: flex; align-items: cente">
+            <el-input
+              v-model.number="form.newMonthValue"
+              placeholder="请输入企业月均工业产值"
+              clearable
+            />
+            <p style="flex: 0 0 auto; margin: 0 10px">万元</p>
+          </div>
+        </el-form-item>
+        <el-form-item label="行业代码" label-width="100px" prop="selectedCode">
+          <el-select v-model="form.selectedCode" style="width: 100%" filterable>
+            <el-option
+              v-for="item in selectedIndustryArray"
+              :key="item.key"
+              :label="item.value"
+              :value="item.key"
+            >
+            </el-option> </el-select
+        ></el-form-item>
+        <el-form-item
+          label="新增企业年实缴税金"
+          label-width="200px"
+          prop="newYearTax"
+        >
+          <!-- <el-input
+            v-model="form.newYearTax"
+            placeholder="请输入企业年实缴税金"
+            clearable
+          /> -->
+          <div style="display: flex; align-items: cente">
+            <el-input
+              v-model.number="form.newYearTax"
+              placeholder="请输入企业年实缴税金"
+              clearable
+            />
+            <p style="flex: 0 0 auto; margin: 0 10px">万元</p>
+          </div>
+        </el-form-item>
+        <el-form-item label-width="100px">
+          <el-button
+            type="primary"
+            icon="el-icon-search"
+            size="mini"
+            @click="submit"
+            >搜索</el-button
+          >
+          <el-button icon="el-icon-refresh" size="mini" @click="resetForm"
+            >重置</el-button
+          >
+        </el-form-item>
+      </el-form>
+    </div>
+    <div ref="chart" :style="{ height: height, width: width }"></div>
+    <el-table :data="tableData" border style="width: 100%">
+      <el-table-column prop="year" label="年度" width="200"> </el-table-column>
+      <el-table-column prop="enterpriseId" label="企业ID"></el-table-column>
+      <el-table-column
+        prop="totalIndustrialValue"
+        label="企业月均工业产值"
+        width="200"
+      ></el-table-column>
+      <el-table-column
+        prop="value"
+        label="Z-score得分"
+        width="200"
+      ></el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+import * as echarts from "echarts";
+import { listAllIndustry } from "@/api/industry/industry"; // 导入行业数据的接口
+import { listAllIndustry_run } from "@/api/industry_run/industry_run"; // 导入行业数据的接口
+require("echarts/theme/macarons"); // echarts theme
+import resize from "@/views/dashboard/mixins/resize";
+import { Decimal } from "decimal.js";
+
+export default {
+  mixins: [resize],
+  props: {
+    className: {
+      type: String,
+      default: "chart",
+    },
+    width: {
+      type: String,
+      default: "100%",
+    },
+    height: {
+      type: String,
+      default: "400px",
+    },
+  },
+  data() {
+    return {
+      chart: null,
+      chartData: [],
+      industryData: [],
+      industryMap: new Map(),
+      selectedDataKey: "totalIndustrialValueScore", // 默认选择的字段
+      form: {
+        selectedCode: null,
+        newMonthValue: null,
+        newYearTax: null,
+      },
+      rules: {
+        selectedCode: [
+          { required: true, message: "请选择行业", trigger: "blur" },
+        ],
+        newMonthValue: [
+          {
+            required: true,
+            message: "请输入企业月均工业产值",
+            trigger: "blur",
+          },
+          { type: "number", message: "企业月均工业产值必须为数字" },
+        ],
+        newYearTax: [
+          {
+            type: "number",
+            message: "企业年实缴税金必须为数字",
+            trigger: "blur",
+          },
+        ],
+      },
+      selectedIndustryArray: [],
+      // dataKeys: [
+      //   "totalIndustrialValueScore",
+      //   // 'gdpScore',
+      //   "taxableIncomeScore",
+      //   "paidTaxScore",
+      //   "fundingScore",
+      //   "energyConsumeScore",
+      //   "powerConsumeScore",
+      //   // 其他得分字段...
+      // ], // 可选项
+      // keyToChinese: {
+      //   mainBusinessScore: "主营业务活动得分",
+      //   landAreaScore: "用地面积得分",
+      //   totalIndustrialValueScore: "工业总产值得分",
+      //   gdpScore: "工业增加值得分",
+      //   taxableIncomeScore: "应税收入得分",
+      //   paidTaxScore: "实缴税金得分",
+      //   mainBusinessIncomeScore: "主营业务收入得分",
+      //   employeeNumberScore: "从业人员数得分",
+      //   profitScore: "利润总额得分",
+      //   ownerEquityScore: "所有者权益得分",
+      //   fundingScore: "研发经费得分",
+      //   energyConsumeScore: "能源消费量得分",
+      //   powerConsumeScore: "电力消费量得分",
+      // },
+      // keyToLevel: {
+      //   totalIndustrialValueScore: "totalIndustrialValueLevel",
+      //   taxableIncomeScore: "taxableIncomeLevel",
+      //   paidTaxScore: "paidTaxLevel",
+      //   fundingScore: "fundingLevel",
+      //   energyConsumeScore: "energyConsumeLevel",
+      //   powerConsumeScore: "powerConsumeLevel",
+      // },
+      selectedYear: null,
+      // queryParams: {
+      //   pageNum: 1,
+      //   pageSize: 2000000, // 默认 2000000 条(全部一次性数据)
+      //   enterpriseName: null,
+      //   location: null,
+      //   code: null,
+      //   mainBusiness: null,
+      //   landArea: null,
+      //   totalIndustrialValue: null,
+      //   gdp: null,
+      //   taxableIncome: null,
+      //   paidTax: null,
+      //   mainBusinessIncome: null,
+      //   employeeNumber: null,
+      //   profit: null,
+      //   ownerEquity: null,
+      //   funding: null,
+      //   energyConsume: null,
+      //   year: null,
+      //   month: null,
+      // },
+      // industryQueryParams: {
+      //   pageNum: 1,
+      //   pageSize: 2000000,
+      //   industryName: null,
+      //   code: null,
+      // }, //  查询行业信息
+      // availableYears: [],
+      selectedRange: 0.1, // 不能为 0, - littlegreen - 固定0.1
+      // littlegreen - 检测区间 和 表格数据
+      detectMin: null,
+      detectMax: null,
+      tableData: [],
+      scoreData: [],
+    };
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.fetchData();
+    });
+  },
+  beforeDestroy() {
+    if (this.chart) {
+      this.chart.dispose();
+    }
+  },
+  methods: {
+    // 拉取初始数据
+    fetchData() {
+      const that = this;
+      // 获得所有行业
+      that.selectedYear = new Date().getUTCFullYear() + "";
+      listAllIndustry()
+        .then((res) => {
+          if (res && res?.rows) {
+            that.selectedIndustryArray = res.rows.map((item) => {
+              return {
+                key: item.code,
+                value: item.code + item.industryName,
+              };
+            });
+          }
+        })
+        .catch((error) => {
+          console.error("Error fetching data:", error);
+        });
+    },
+    // 提交表单
+    submit() {
+      this.$refs.ruleForm.validate((valid) => {
+        if (valid) {
+          this.handleData();
+        } else {
+          console.error("error submit!!");
+          return false;
+        }
+      });
+    },
+    // 处理数据
+    handleData() {
+      // 1.拿当前年份和code的企业运行数据过来
+      // 2.计算每个企业的z-score
+      // 3.画图
+      const that = this;
+      listAllIndustry_run({
+        code: that.form.selectedCode,
+        year: that.selectedYear,
+      })
+        .then((res) => {
+          if (res && res.rows) {
+            that.scoreData = res.rows;
+            that.initChart();
+          }
+        })
+        .catch((error) => {
+          console.error("Error fetching data:", error);
+        });
+    },
+    initChart() {
+      if (this.chart) {
+        this.chart.dispose();
+      }
+      this.chart = echarts.init(this.$refs.chart, "macarons");
+      this.updateChart();
+    },
+    updateChart() {
+      const that = this;
+      if (!this.chart) {
+        return;
+      }
+      let newMonthValue = parseFloat(that.form.newMonthValue);
+      let total = [parseFloat(newMonthValue)]; //总值
+      const result = Object.values(
+        that.scoreData.reduce((acc, item) => {
+          const key = item.enterpriseName; // 根据企业名称分组
+          if (!acc[key]) {
+            acc[key] = { name: key, data: [], totalValue: 0, count: 0 }; // 创建对象并初始化
+          }
+          acc[key].data.push(item); // 添加到对应的类别中
+          acc[key].totalValue += item.totalIndustrialValue; // 累加 totalIndustrialValue
+          acc[key].count++; // 企业数 +1
+          return acc;
+        }, {})
+      ).map(({ name, data, totalValue, count }) => {
+        const totalIndustrialValue = totalValue / count; // 计算均值
+        total.push(totalIndustrialValue);
+        return { name, data, totalIndustrialValue }; // 返回结果
+      });
+      result.push({
+        name: "新企业",
+        data: [],
+        totalIndustrialValue: newMonthValue,
+      });
+      // 获取基础数据:最大值,最小值,平均值,标准差
+      function getBebeQ(numbers, digit = 2) {
+        let sum = numbers.reduce(
+          (acc, num) => acc.add(new Decimal(num)),
+          new Decimal(0)
+        );
+
+        let max = Math.max.apply(null, numbers);
+        let min = Math.min.apply(null, numbers);
+
+        // 平均值
+        let mean = sum.dividedBy(numbers.length);
+
+        // 计算每个数与均值的差的平方
+        const squaredDiffs = numbers.map((num) => {
+          const diff = new Decimal(num).minus(mean);
+          return diff.pow(2);
+        });
+
+        // 计算平方差的均值
+        const sumOfSquaredDiffs = squaredDiffs.reduce(
+          (acc, diff) => acc.add(diff),
+          new Decimal(0)
+        );
+
+        const variance = sumOfSquaredDiffs.dividedBy(numbers.length);
+
+        // 开平方得到标准差
+        const standardDeviation = variance.sqrt();
+
+        // 向上取整到最近的 0.1
+        const ceilingRoundedMax = Math.ceil(max * 10) / 10;
+
+        // 向下取整到最近的 0.1
+        const floorRoundedMin = Math.floor(min * 10) / 10;
+
+        return {
+          max: ceilingRoundedMax,
+          min: floorRoundedMin,
+          avg: parseFloat(mean.toNumber().toFixed(digit)) || 0,
+          stdDev: parseFloat(standardDeviation.toNumber().toFixed(digit)),
+        };
+      }
+
+      // 示例数据
+      const betaData = getBebeQ(total);
+      let newZScore;
+      const values = result.map((item) => {
+        let num = item.totalIndustrialValue;
+        const zScore = new Decimal(num)
+          .minus(betaData.avg)
+          .dividedBy(betaData.stdDev);
+        let scoreNum = zScore.toNumber();
+        if (new Decimal(num).equals(new Decimal(that.form.newMonthValue))) {
+          newZScore = scoreNum;
+        }
+        item.score = scoreNum;
+        return scoreNum;
+      });
+      // 计算 z-score 的基础数据
+      const scoreBasic = getBebeQ(values);
+      const center = new Decimal(scoreBasic.avg); // 确保 center 是 Decimal 类型
+      const interval = new Decimal(0.1);
+
+      // 使用 Decimal 来处理 minLimit 和 maxLimit
+      let minLimit = new Decimal(scoreBasic.min); // 向下取整到最近的0.1
+      let maxLimit = new Decimal(scoreBasic.max); // 向上取整到最近的0.1
+
+      // 确保 minLimit 和 maxLimit 关于 center 对称
+      if (!center.minus(minLimit).equals(maxLimit.minus(center))) {
+        const distanceToCenter = center
+          .minus(minLimit)
+          .gt(maxLimit.minus(center))
+          ? center.minus(minLimit)
+          : maxLimit.minus(center);
+
+        maxLimit = center.plus(distanceToCenter); // 更新 maxLimit 保持对称
+        minLimit = center.minus(distanceToCenter); // 更新 minLimit 保持对称
+      }
+
+      const intervals = [];
+      // 乘以1000 是将小数变成整数去计算,小数计算会有误差
+      for (
+        let i = minLimit * 1000;
+        i <= maxLimit * 1000;
+        i += interval * 1000
+      ) {
+        intervals.push(parseFloat((i / 1000).toFixed(2)));
+      }
+      // // 计算每个间隔的频次
+      const frequency = new Array(intervals.length - 1).fill(0); // 初始化频次数组
+
+      let intervalArr = intervals.slice(0, -1);
+      const intervalStrings = [];
+      for (let i = 0; i < intervalArr.length; i++) {
+        const start = intervalArr[i].toFixed(1); // 保留一位小数
+        const end = ((intervalArr[i] * 1000 + interval * 1000) / 1000).toFixed(
+          1
+        ); // 保留一位小数
+        intervalStrings.push({
+          name: `${start}-${end}`,
+          max: end,
+          min: start,
+        }); // 构建区间字符串
+      }
+
+      values.forEach((value) => {
+        for (let j = 0; j < intervalStrings.length; j++) {
+          // 保证包含右边界
+          let v = parseFloat(value) * 1000;
+          if (
+            v >= parseFloat(intervalStrings[j].min) * 1000 &&
+            v < parseFloat(intervalStrings[j].max) * 1000
+          ) {
+            frequency[j]++;
+            break; // 找到对应区间后可以跳出循环
+          }
+        }
+      });
+
+      // // 确保处理最大值等于maxLimit的情况
+      if (
+        values.some(
+          (value) => parseFloat(value) * 1000 == parseFloat(maxLimit) * 1000
+        )
+      ) {
+        frequency[frequency.length - 1]++;
+      }
+      // 1.计算正态分布值
+      const normalDistributionValues = intervals
+        .slice(0, -1)
+        .map((intervalStart, index) => {
+          const intervalEnd = intervals[index + 1];
+          const midpoint = (intervalStart + intervalEnd) / 2;
+
+          // 使用 Decimal 进行更高精度的计算
+          const midPointDecimal = new Decimal(midpoint);
+          const avgDecimal = new Decimal(scoreBasic.avg);
+          const stdDevDecimal = new Decimal(scoreBasic.stdDev);
+
+          const exponent = midPointDecimal
+            .minus(avgDecimal)
+            .dividedBy(stdDevDecimal)
+            .pow(2)
+            .negated()
+            .dividedBy(new Decimal(2));
+          const exponentValue = new Decimal(Math.exp(exponent.toNumber())); // 取自然指数
+          const coefficient = new Decimal(1).dividedBy(
+            stdDevDecimal.times(new Decimal(Math.sqrt(2 * Math.PI)))
+          );
+
+          return coefficient.times(exponentValue).toNumber(); // 返回计算结果
+        });
+      const option = {
+        color: ["rgba(245,0,0,0.6)"],
+        dataZoom: [
+          {
+            type: "inside", // 内置于坐标系中
+            start: 0,
+            end: 100,
+            xAxisIndex: [0],
+          },
+        ],
+        tooltip: {
+          trigger: "axis",
+          axisPointer: {
+            type: "shadow",
+          },
+          formatter: (params) => {
+            // 使用箭头函数来保持 this 的上下文
+            const seriesName = params[0].name; // 系列名称
+            const dataIndex = params[0].dataIndex; // 数据索引
+            const value = params[0].data; // 实际的数据值
+            const xAxisLabel = params[0].axisValueLabel; // x轴的标签
+            const yAxisLabel = params[0].seriesName;
+            // 构建自定义的 tooltip 内容,包含年份
+            return `${xAxisLabel}</br> ${yAxisLabel}: ${value} `;
+          },
+        },
+        legend: {
+          data: [`${that.selectedYear}工业生产总值企业数统计`, "正态分布"],
+        },
+        grid: {
+          left: "3%",
+          right: "4%",
+          bottom: "50px",
+          containLabel: true,
+        },
+        xAxis: {
+          type: "category",
+          name: "分布区间",
+          // data: histogramData.map((item) => item.name),
+          data: intervalStrings.map((item) => item.name),
+          axisLabel: {
+            fontSize: 12,
+            rotate: 0,
+            // padding: [0, 0, 0, -200]
+          },
+        },
+        yAxis: [
+          {
+            name: "企业数",
+            type: "value",
+            minInterval: 1,
+          },
+          {
+            name: "正态分布",
+            type: "value",
+          },
+        ],
+        series: [
+          {
+            name: `${that.selectedYear}工业生产总值企业数统计`,
+            type: "bar",
+            barWidth: "30%",
+            itemStyle: {
+              // color: "rgba(245,0,0,0.6)",
+              normal: {
+                color: function (params) {
+                  const value = params.dataIndex; // 获取当前柱子在数据中的索引
+                  let min = parseFloat(intervalStrings[value].min) * 1000;
+                  let max = parseFloat(intervalStrings[value].max) * 1000;
+                  if (newZScore * 1000 >= min && newZScore * 1000 < max) {
+                    return "#ffcc00";
+                  } else if (
+                    newZScore * 1000 == maxLimit * 1000 &&
+                    max == maxLimit * 1000
+                  ) {
+                    return "#ffcc00";
+                  }
+                  return "rgba(245,0,0,0.6)";
+                  // 默认颜色
+                },
+              },
+            },
+            // data: histogramData.map((item) => item.value),
+            data: frequency,
+            label: {
+              show: false, // 将show属性设置为false,去掉柱子上的数字
+            },
+          },
+          {
+            name: "正态分布",
+            yAxisIndex: 1,
+            type: "line",
+            data: normalDistributionValues,
+            lineStyle: {
+              color: "#1a7cc8",
+            },
+            symbol: "none",
+          },
+        ],
+      };
+      this.chart.setOption(option);
+      this.chart.on("click", function (param) {
+        if (
+          typeof param.dataIndex === "number" &&
+          param.dataIndex >= 0 &&
+          param.dataIndex < intervalStrings.length
+        ) {
+          let dataIndexValue = intervalStrings[param.dataIndex];
+          const filteredArray = result.filter((item) => {
+            let score = new Decimal(item.score);
+            let max = new Decimal(dataIndexValue.max);
+            let min = new Decimal(dataIndexValue.min);
+            let maxL = new Decimal(maxLimit);
+            const isMaxValEqual = max.equals(maxL);
+            const lowerBoundCheck = score.greaterThan(min) || score.equals(min);
+            const upperBoundCheck = isMaxValEqual
+              ? score.lessThan(max) || score.equals(max)
+              : score.lessThan(max);
+            return lowerBoundCheck && upperBoundCheck;
+          });
+          that.tableData = filteredArray.map((item) => {
+            return {
+              totalIndustrialValue: item.totalIndustrialValue.toFixed(2),
+              enterpriseId: item.name,
+              year: that.selectedYear,
+              value: item.score.toFixed(2)
+            };
+          });
+        }
+      });
+    },
+    // 重置表单
+    resetForm() {
+      this.$refs.ruleForm.resetFields();
+    },
+  },
+  watch: {},
+};
+</script>
+
+<style scoped>
+.select-container {
+  margin: 10px 0;
+  font-size: 16px; /* 调整字号大小 */
+}
+
+.select-container select {
+  padding: 10px 20px; /* 增加内边距 */
+  margin-right: 10px; /* 增加右边距 */
+  border: 1px solid #121315; /* 蓝色边框 */
+  border-radius: 4px; /* 圆角边框 */
+  background-color: white; /* 背景色 */
+  color: #121315; /* 文字颜色 */
+  font-size: 16px; /* 调整字号大小 */
+  cursor: pointer; /* 鼠标悬停时的指针样式 */
+  outline: none; /* 移除焦点时的轮廓 */
+}
+
+.select-container select:hover {
+  border-color: #1a7cc8; /* 鼠标悬停时的边框颜色 */
+}
+
+.select-container select:focus {
+  border-color: #121315; /* 焦点时的边框颜色 */
+  box-shadow: 0 0 0 2px rgba(64, 159, 255, 0.2); /* 焦点时的阴影效果 */
+}
+</style>