| | |
| | | <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> |
| | | |