zhang_12370
4 天以前 e986cee1c804ecdf6d03c080ce9a8bb187f724a4
src/views/index.vue
@@ -1,14 +1,743 @@
<template>
  <div class="app-container">
  <div class="dashboard">
    <!-- 顶部统计卡片 -->
    <div class="top-cards">
      <div class="stat-card revenue">
        <div class="card-icon">
          <i class="el-icon-money"></i>
        </div>
        <div class="card-content">
          <div class="card-title">营收金额</div>
          <div class="card-value">
            ¥{{
              homePageData.revenueAmount
                ? formatThousand(homePageData.revenueAmount)
                : "--"
            }}
          </div>
          <div class="card-trend" v-if="homePageData.trend == '+'">
            <span class="trend-label">较昨日</span>
            <span class="trend-value up">+ {{ homePageData.changeRate }}</span>
          </div>
          <div class="card-trend" v-if="homePageData.trend == '-'">
            <span class="trend-label">较昨日</span>
            <span class="trend-value down"
              >- {{ homePageData.changeRate }}</span
            >
          </div>
        </div>
      </div>
      <div class="stat-card supply">
        <div class="card-icon">
          <i class="el-icon-truck"></i>
        </div>
        <div class="card-content">
          <div class="card-title">供应量</div>
          <div class="card-value">
            {{
              homePageData.saleQuantity
                ? formatThousand(homePageData.saleQuantity)
                : "--"
            }}吨
          </div>
          <div class="card-trend" v-if="homePageData.trendQuantity == '+'">
            <span class="trend-label">较昨日</span>
            <span class="trend-value up"
              >+ {{ homePageData.saleQuantityRate }}</span
            >
          </div>
          <div class="card-trend" v-if="homePageData.trendQuantity == '-'">
            <span class="trend-label">较昨日</span>
            <span class="trend-value down"
              >- {{ homePageData.saleQuantityRate }}</span
            >
          </div>
        </div>
      </div>
    </div>
    <!-- 中间图表区域 -->
    <div class="chart-section">
      <div class="chart-container">
        <div class="chart-title">营收分布</div>
        <div ref="pieChart" class="chart-content pie-chart"></div>
      </div>
      <div class="chart-container">
        <div class="chart-title">
          <span>供应量趋势</span>
          <div>
            <el-date-picker
              :locale="zhCN"
              v-model="selectMonth"
              type="monthrange"
              placeholder="选择日期"
              format="YYYY/MM"
              value-format="YYYY-MM"
              range-separator="至"
              start-placeholder="开始日期"
              end-placeholder="结束日期"
              @change="searchMonth"
            />
          </div>
        </div>
        <div ref="areaChart" class="chart-content area-chart"></div>
      </div>
    </div>
    <!-- 底部三栏布局 -->
    <div class="bottom-section">
      <!-- 库存统计 -->
      <div class="bottom-card inventory">
        <div class="card-header">
          <h3>库存统计</h3>
        </div>
        <div class="inventory-items">
          <div class="inventory-item" v-for="(item, index) in inventoryList.Yvalues" :key="index">
            <div class="item-name">{{ inventoryList.Xkeys[index]? inventoryList.Xkeys[index] : "--"}}</div>
            <div class="item-value">{{ item ? formatThousand(item) : "0" }}</div>
            <div class="item-status">吨</div>
          </div>
        </div>
      </div>
      <!-- 柱状图 -->
      <div class="bottom-card chart">
        <div class="card-header">
          <h3>月度对比</h3>
        </div>
        <div ref="barChart" class="chart-content bar-chart"></div>
      </div>
      <!-- 销售数据表格 -->
      <div class="bottom-card table">
        <div class="card-header">
          <h3>销售数据</h3>
        </div>
        <el-table
          :data="salesData"
          style="width: 100%"
          :header-cell-style="tableHeaderStyle"
        >
          <el-table-column
            prop="coalName"
            label="产品"
            align="center"
            mini-width="50"
          ></el-table-column>
          <el-table-column
            prop="inventoryQuantity"
            label="数量"
            align="center"
            mini-width="50"
          ></el-table-column>
          <el-table-column
            prop="totalAmount"
            label="金额"
            align="center"
            mini-width="50"
          ></el-table-column>
        </el-table>
      </div>
    </div>
  </div>
</template>
<script setup name="Index">
<!-- 删除多余的 script 结束标签 -->
<script setup>
import { getCoalInfo, getYearlySales } from "@/api/home/index";
import { ref, onMounted, nextTick } from "vue";
import zhCn from "element-plus/dist/locale/zh-cn.mjs";
// 兼容模板变量名,暴露给模板使用
const zhCN = zhCn;
import * as echarts from "echarts";
const homePageData = ref({});
const selectMonth = ref([]);
// 生成无限随机颜色(HSL算法保证高辨识度、柔和不刺眼)
function generateRandomColors(count = 10) {
  const colors = [];
  const goldenAngle = 137.508; // 黄金角度,保证颜色分布均匀
  for (let i = 0; i < count; i++) {
    // 使用黄金角度分割确保颜色差异大
    const hue = (i * goldenAngle) % 360;
    // 饱和度:40-70% 避免过于鲜艳
    const saturation = 40 + Math.random() * 30;
    // 明度:45-75% 避免过暗或过亮
    const lightness = 45 + Math.random() * 30;
    colors.push(
      `hsl(${Math.round(hue)}, ${Math.round(saturation)}%, ${Math.round(
        lightness
      )}%)`
    );
  }
  return colors;
}
// HSL转16进制(可选,如果需要hex格式)
function hslToHex(hsl) {
  const match = hsl.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/);
  if (!match) return hsl;
  const h = parseInt(match[1]) / 360;
  const s = parseInt(match[2]) / 100;
  const l = parseInt(match[3]) / 100;
  const hue2rgb = (p, q, t) => {
    if (t < 0) t += 1;
    if (t > 1) t -= 1;
    if (t < 1 / 6) return p + (q - p) * 6 * t;
    if (t < 1 / 2) return q;
    if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
    return p;
  };
  let r, g, b;
  if (s === 0) {
    r = g = b = l;
  } else {
    const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
    const p = 2 * l - q;
    r = hue2rgb(p, q, h + 1 / 3);
    g = hue2rgb(p, q, h);
    b = hue2rgb(p, q, h - 1 / 3);
  }
  const toHex = (c) => {
    const hex = Math.round(c * 255).toString(16);
    return hex.length === 1 ? "0" + hex : hex;
  };
  return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
// 便捷方法:直接获取16进制颜色数组
function getRandomHexColors(count = 1) {
  return generateRandomColors(count).map(hslToHex);
}
// 千分位格式化函数
function formatThousand(num) {
  if (typeof num === "number") return num.toLocaleString();
  if (typeof num === "string") {
    const n = Number(num.replace(/,/g, ""));
    if (isNaN(n)) return num;
    return n.toLocaleString();
  }
  return num;
}
// 销售数据原始
const salesData = ref([]);
const tableHeaderStyle = {
  backgroundColor: "#f5f7fa",
  color: "#606266",
  fontSize: "12px",
};
// 图表ref
const pieChart = ref(null);
const areaChart = ref(null);
const barChart = ref(null);
// 饼图初始化
const initPieChart = () => {
  const chart = echarts.init(pieChart.value);
  const option = {
    tooltip: {
      trigger: "item",
      formatter: "{a} <br/>{b}: {c} ({d}%)",
    },
    legend: {
      orient: "vertical",
      left: "right",
      top: "center",
      textStyle: {
        fontSize: 12,
      },
    },
    series: [
      {
        name: "营收分布",
        type: "pie",
        radius: ["30%", "70%"],
        center: ["40%", "50%"],
        avoidLabelOverlap: false,
        label: {
          show: false,
          position: "center",
        },
        emphasis: {
          label: {
            show: true,
            fontSize: "16",
            fontWeight: "bold",
          },
        },
        labelLine: {
          show: false,
        },
        data: revenueDistribution.value,
      },
    ],
  };
  chart.setOption(option);
  window.addEventListener("resize", () => {
    chart.resize();
  });
};
// 面积图初始化
const initAreaChart = () => {
  const chart = echarts.init(areaChart.value);
  const option = {
    title: {
      show: supplyTrend.value.length == 0, // 没数据才显示
      extStyle: {
        color: "grey",
        fontSize: 20,
      },
      text: "暂无数据",
      left: "center",
      top: "center",
    },
    tooltip: {
      trigger: "axis",
      axisPointer: {
        type: "cross",
        label: {
          backgroundColor: "#6a7985",
        },
      },
    },
    legend: {
      data: ["供应量"],
      top: 10,
    },
    grid: {
      left: "3%",
      right: "4%",
      bottom: "3%",
      containLabel: true,
    },
    xAxis: [
      {
        type: "category",
        boundaryGap: false,
        data: supplyTrend.value.Xkeys || [],
        axisLabel: {
          fontSize: 12,
        },
      },
    ],
    yAxis: [
      {
        type: "value",
        axisLabel: {
          fontSize: 12,
        },
      },
    ],
    series: [
      {
        name: "供应量",
        type: "line",
        stack: "Total",
        areaStyle: {
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "rgba(64, 158, 255, 0.3)" },
            { offset: 1, color: "rgba(64, 158, 255, 0.1)" },
          ]),
        },
        emphasis: {
          focus: "series",
        },
        data: supplyTrend.value.Yvalues || [],
        lineStyle: {
          color: "#409EFF",
        },
        itemStyle: {
          color: "#409EFF",
        },
      },
    ],
  };
  chart.setOption(option);
  window.addEventListener("resize", () => {
    chart.resize();
  });
};
// 柱状图初始化
const initBarChart = () => {
  const chart = echarts.init(barChart.value);
  const option = {
    tooltip: {
      trigger: "axis",
      axisPointer: {
        type: "shadow",
      },
    },
    grid: {
      left: "3%",
      right: "4%",
      bottom: "3%",
      containLabel: true,
    },
    xAxis: {
      type: "category",
      data: resultMonthList.value.Xkeys || [],
      axisLabel: {
        fontSize: 11,
      },
    },
    yAxis: {
      type: "value",
      axisLabel: {
        fontSize: 11,
      },
    },
    series: [
      {
        name: "销量",
        type: "bar",
        data: resultMonthList.value.Yvalues || [],
        itemStyle: {
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: "#409EFF" },
            { offset: 1, color: "#79bbff" },
          ]),
        },
        barWidth: "60%",
      },
    ],
  };
  chart.setOption(option);
  window.addEventListener("resize", () => {
    chart.resize();
  });
};
// 收入分布数据
const revenueDistribution = ref([]);
// 初始化所有图表
const initCharts = () => {
  initPieChart();
  initAreaChart();
  initBarChart();
};
const getList = async () => {
  try {
    searchMonth();
    const res = await getCoalInfo();
    homePageData.value = res.data || {};
    revenueDistribution.value = [];
    if (homePageData.value.revenueDistribution) {
      Object.keys(homePageData.value.revenueDistribution).forEach((key) => {
        let obj = {};
        obj.name = key;
        obj.value = homePageData.value.revenueDistribution[key];
        obj.itemStyle = {
          color: getRandomHexColors(1)[0], // 使用随机颜色
        };
        revenueDistribution.value.push(obj);
      });
    }
    if (homePageData.value.inventory) {
      let inventoryListXkeys = Object.keys(homePageData.value.inventory);
      let inventoryListYvalues = Object.values(homePageData.value.inventory);
      inventoryList.value = {
        Xkeys: inventoryListXkeys,
        Yvalues: inventoryListYvalues,
      };
    }
    if(homePageData.value.resultMouth){
      let resultMonthXkeys = Object.keys(homePageData.value.resultMouth);
      let resultMonthYvalues = Object.values(homePageData.value.resultMouth);
      resultMonthList.value = {
        Xkeys: resultMonthXkeys,
        Yvalues: resultMonthYvalues,
      };
      console.log(resultMonthList.value);
    }
    if(homePageData.value.salesResults){
      salesData.value = homePageData.value.salesResults;
    }
    // 数据加载完成后重新初始化图表
    nextTick(() => {
      initCharts();
    });
  } catch (error) {
    console.error("获取煤种信息失败:", error);
  }
};
const inventoryList = ref([]);
const resultMonthList = ref([]);
const supplyTrend = ref({});
const searchMonth = async () => {
  let res = await getYearlySales({
    timeRange: selectMonth.value ? selectMonth.value : null,
  });
  let Xkeys = Object.keys(res.data.data);
  let Yvalues = Object.values(res.data.data);
  supplyTrend.value = {
    Xkeys,
    Yvalues,
  };
  nextTick(() => {
      initAreaChart();
    });
};
onMounted(() => {
  getList();
});
</script>
<style scoped lang="scss">
.dashboard {
  padding: 20px;
  background-color: #f5f7fa;
  min-height: 91vh;
  box-sizing: border-box;
}
/* 顶部统计卡片 */
.top-cards {
  display: flex;
  gap: 20px;
  margin-bottom: 20px;
}
.stat-card {
  flex: 1;
  background: white;
  border-radius: 8px;
  padding: 20px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  display: flex;
  align-items: center;
  gap: 15px;
}
.card-icon {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
  color: white;
}
.revenue .card-icon {
  background: linear-gradient(135deg, #409eff, #79bbff);
}
.supply .card-icon {
  background: linear-gradient(135deg, #67c23a, #95d475);
}
.card-content {
  flex: 1;
}
.card-title {
  font-size: 14px;
  color: #909399;
  margin-bottom: 8px;
}
.card-value {
  font-size: 24px;
  font-weight: bold;
  color: #303133;
  margin-bottom: 5px;
}
.card-trend {
  font-size: 12px;
}
.trend-label {
  color: #909399;
  margin-right: 5px;
}
.trend-value.up {
  color: #67c23a;
}
.trend-value.down {
  color: #f56c6c;
}
/* 中间图表区域 */
.chart-section {
  display: flex;
  gap: 20px;
  margin-bottom: 20px;
}
.el-scrollbar__view {
  width: 100%;
}
.chart-container {
  flex: 1;
  background: white;
  border-radius: 8px;
  padding: 20px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.chart-title {
  font-size: 16px;
  font-weight: bold;
  color: #303133;
  margin-bottom: 15px;
  padding-bottom: 10px;
  border-bottom: 2px solid #f0f0f0;
  display: flex;
  justify-content: space-between;
}
.chart-content {
  height: 280px;
}
/* 底部三栏布局 */
.bottom-section {
  display: flex;
  gap: 20px;
}
.bottom-card {
  flex: 1;
  background: white;
  border-radius: 8px;
  padding: 20px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card-header {
  margin-bottom: 15px;
  padding-bottom: 10px;
  border-bottom: 2px solid #f0f0f0;
}
.card-header h3 {
  margin: 0;
  font-size: 16px;
  font-weight: bold;
  color: #303133;
}
/* 库存统计样式 */
.inventory-items {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.inventory-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px;
  background: #f8f9fa;
  border-radius: 6px;
  border-left: 3px solid #409eff;
}
.item-name {
  font-weight: bold;
  color: #303133;
}
.item-value {
  color: #606266;
  font-size: 14px;
}
.item-status {
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: bold;
}
.item-status.normal {
  background: #f0f9ff;
  color: #67c23a;
}
.item-status.low {
  background: #fef0e6;
  color: #e6a23c;
}
/* 柱状图容器 */
.bar-chart {
  height: 200px;
}
/* 表格样式调整 */
.bottom-card.table {
  width: 100%;
}
.bottom-card.table .el-table {
  font-size: 12px;
}
.bottom-card.table .el-table td,
.bottom-card.table .el-table th {
  padding: 8px 0;
}
:deep(.el-scrollbar__view) {
  width: 100% !important;
}
:deep(.el-table__header, ) {
  width: 100% !important;
}
:deep(.el-table__body, ) {
  width: 100% !important;
}
/* 响应式设计 */
@media (max-width: 1200px) {
  .bottom-section {
    flex-direction: column;
  }
  .chart-section {
    flex-direction: column;
  }
}
@media (max-width: 768px) {
  .top-cards {
    flex-direction: column;
  }
  .dashboard {
    padding: 10px;
  }
  .stat-card {
    padding: 15px;
  }
  .card-value {
    font-size: 20px;
  }
}
</style>