<template>
|
<div ref="screenRoot"
|
class="sales-statistics-container"
|
:class="{ 'is-fullscreen': isFullscreen }">
|
<div class="bi-bg"></div>
|
<div class="bi-topbar">
|
<img class="bi-topbar-title-bg"
|
src="@/assets/BI/biaoti.png"
|
alt="销售统计看板" />
|
<div class="bi-topbar-content">
|
<div class="bi-topbar-left">
|
<button class="fullscreen-btn"
|
@click="toggleFullscreen"
|
:title="isFullscreen ? '退出全屏' : '全屏显示'">
|
<svg v-if="!isFullscreen"
|
width="1.6vh"
|
height="1.6vh"
|
viewBox="0 0 24 24"
|
fill="none"
|
stroke="currentColor"
|
stroke-width="2">
|
<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3" />
|
</svg>
|
<svg v-else
|
width="1.6vh"
|
height="1.6vh"
|
viewBox="0 0 24 24"
|
fill="none"
|
stroke="currentColor"
|
stroke-width="2">
|
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" />
|
</svg>
|
</button>
|
<!-- <span class="status-sun">☀</span>
|
<span>26℃</span>
|
<span class="bi-topbar-sep">湿度:1</span> -->
|
</div>
|
<div class="bi-topbar-title">销售统计看板</div>
|
<div class="bi-topbar-meta">
|
<span class="bi-topbar-time">{{ currentTime }}</span>
|
<span class="bi-topbar-sep">|</span>
|
<span class="bi-topbar-date">{{ currentDateText }}</span>
|
</div>
|
</div>
|
</div>
|
<div class="bi-dashboard-grid">
|
<!-- 左上:销量趋势 -->
|
<div class="bi-panel bi-panel-top-left">
|
<PanelHeader :isFullscreen="true"
|
title="销量分析趋势图" />
|
<div class="panel-tabs">
|
<span class="tab-item"
|
:class="{ active: chartTimeDimension === '年' }"
|
@click="handleChartTimeDimensionChange('年')">年</span>
|
<span class="tab-item"
|
:class="{ active: chartTimeDimension === '月' }"
|
@click="handleChartTimeDimensionChange('月')">月</span>
|
</div>
|
<div class="panel-tabs2">
|
<span class="tab-item"
|
:class="{ active: chartProductType === '砌块' }"
|
@click="handleChartProductTypeChange('砌块')">砌块</span>
|
<span class="tab-item"
|
:class="{ active: chartProductType === '板材' }"
|
@click="handleChartProductTypeChange('板材')">板材</span>
|
</div>
|
<div class="bi-panel-body">
|
<div class="chart-unit-row">
|
<span>单位:立方米</span>
|
</div>
|
<div ref="salesVolumeChart"
|
class="echart-fill"></div>
|
</div>
|
</div>
|
<!-- 右上:销售金额 -->
|
<div class="bi-panel bi-panel-top-right">
|
<PanelHeader :isFullscreen="true"
|
title="销售金额分析" />
|
<div class="panel-tabs">
|
<span class="tab-item"
|
:class="{ active: chartTimeDimension2 === '年' }"
|
@click="handleChartTimeDimensionChange2('年')">年</span>
|
<span class="tab-item"
|
:class="{ active: chartTimeDimension2 === '月' }"
|
@click="handleChartTimeDimensionChange2('月')">月</span>
|
</div>
|
<div class="panel-tabs2">
|
<span class="tab-item"
|
:class="{ active: chartProductType2 === '砌块' }"
|
@click="handleChartProductTypeChange2('砌块')">砌块</span>
|
<span class="tab-item"
|
:class="{ active: chartProductType2 === '板材' }"
|
@click="handleChartProductTypeChange2('板材')">板材</span>
|
</div>
|
<div class="bi-panel-body">
|
<div class="chart-unit-row">
|
<span>单位:元</span>
|
</div>
|
<div ref="salesAmountChart"
|
class="echart-fill"></div>
|
</div>
|
</div>
|
<!-- 中间中心环 -->
|
<div class="center-ring">
|
<div class="center-ring-box">
|
<div class="center-metric m1">
|
<div class="center-metric-label">总销售金额</div>
|
<div class="center-metric-value">{{ totalSalesAmount.toFixed(0) }}</div>
|
<div class="center-metric-unit">万元</div>
|
</div>
|
<div class="center-metric m2">
|
<div class="center-metric-label">成交总订单</div>
|
<div class="center-metric-value">{{ completedOrders }}</div>
|
<div class="center-metric-unit">单</div>
|
</div>
|
<div class="center-metric m3">
|
<div class="center-metric-label">累计客户</div>
|
<div class="center-metric-value">{{ centerNewCustomerCount }}</div>
|
<div class="center-metric-unit">家</div>
|
</div>
|
<div class="center-metric m4">
|
<div class="center-metric-label">销售方数</div>
|
<div class="center-metric-value">{{ totalSalesAreaCount.toFixed(0) }}</div>
|
<div class="center-metric-unit">方</div>
|
</div>
|
</div>
|
</div>
|
<!-- 左下:产品类型销量 -->
|
<div class="bi-panel bi-panel-bottom-left">
|
<PanelHeader :isFullscreen="true"
|
title="销量数据统计" />
|
<div class="panel-tabs">
|
<span class="tab-item"
|
:class="{ active: tableTimeDimension === '年' }"
|
@click="handleTableTimeDimensionChange('年')">年</span>
|
<span class="tab-item"
|
:class="{ active: tableTimeDimension === '月' }"
|
@click="handleTableTimeDimensionChange('月')">月</span>
|
</div>
|
<div class="panel-tabs2">
|
<span class="tab-item"
|
:class="{ active: tableProductType === '砌块' }"
|
@click="handleTableProductTypeChange('砌块')">砌块</span>
|
<span class="tab-item"
|
:class="{ active: tableProductType === '板材' }"
|
@click="handleTableProductTypeChange('板材')">板材</span>
|
</div>
|
<div class="bi-panel-body">
|
<div class="chart-filter-tabs">
|
<span v-for="area in tableSalesAreas"
|
:key="area"
|
class="cf-tab"
|
:class="{ active: tableSelectedArea === area }"
|
@click="handleTableAreaChange(area)">{{ area }}</span>
|
</div>
|
<div class="scroll-table-container">
|
<table class="scroll-table">
|
<thead>
|
<tr>
|
<th>序号</th>
|
<th>产品类型</th>
|
<th>年月</th>
|
<th>销售区</th>
|
<th>销量(m³)</th>
|
</tr>
|
</thead>
|
<div class="scroll-table-content">
|
<tbody ref="blockTableBody">
|
<tr :class="(index + 1) % 2 === 0 ? 'evenTableTr' : 'oddTableTr'"
|
v-for="(item, index) in filteredTableSalesData"
|
:key="item.period + item.area + index">
|
<td>{{ index + 1 }}</td>
|
<td>{{ item.productType }}</td>
|
<td>{{ item.period }}</td>
|
<td>{{ item.area }}</td>
|
<td>{{ item.sales }}</td>
|
</tr>
|
</tbody>
|
</div>
|
</table>
|
</div>
|
<div class="panel-summary-row">
|
<div class="summary-label">合计</div>
|
<div class="summary-value">{{ filteredTableSalesTotal }} m³</div>
|
</div>
|
</div>
|
</div>
|
<!-- 中下:新增客户分析(分产品类型趋势) -->
|
<div class="bi-panel bi-panel-bottom-center">
|
<PanelHeader :isFullscreen="true"
|
title="新增客户趋势分析" />
|
<div class="panel-tabs">
|
<span class="tab-item"
|
:class="{ active: customerTimeDimension === '年' }"
|
@click="handleCustomerTimeDimensionChange('年')">年</span>
|
<span class="tab-item"
|
:class="{ active: customerTimeDimension === '月' }"
|
@click="handleCustomerTimeDimensionChange('月')">月</span>
|
</div>
|
<div class="bi-panel-body">
|
<div class="chart-unit-row chart-unit-single">
|
<span>单位:家</span>
|
</div>
|
<div ref="productTypeTrendChart"
|
class="echart-fill"></div>
|
</div>
|
</div>
|
<!-- 右下:销售区域销量 -->
|
<div class="bi-panel bi-panel-bottom-right">
|
<PanelHeader :isFullscreen="true"
|
title="销售额数据统计" />
|
<div class="panel-tabs">
|
<span class="tab-item"
|
:class="{ active: tableTimeDimension2 === '年' }"
|
@click="handleTableTimeDimensionChange2('年')">年</span>
|
<span class="tab-item"
|
:class="{ active: tableTimeDimension2 === '月' }"
|
@click="handleTableTimeDimensionChange2('月')">月</span>
|
</div>
|
<div class="panel-tabs2">
|
<span class="tab-item"
|
:class="{ active: tableProductType2 === '砌块' }"
|
@click="handleTableProductTypeChange2('砌块')">砌块</span>
|
<span class="tab-item"
|
:class="{ active: tableProductType2 === '板材' }"
|
@click="handleTableProductTypeChange2('板材')">板材</span>
|
</div>
|
<div class="bi-panel-body">
|
<div class="chart-filter-tabs">
|
<span v-for="area in tableSalesAreas"
|
:key="area"
|
class="cf-tab"
|
:class="{ active: tableSelectedArea === area }"
|
@click="handleTableAreaChange2(area)">{{ area }}</span>
|
</div>
|
<div class="scroll-table-container">
|
<table class="scroll-table">
|
<thead>
|
<tr>
|
<th>序号</th>
|
<th>产品类型</th>
|
<th>年月</th>
|
<th>销售区</th>
|
<th>销售额(元)</th>
|
</tr>
|
</thead>
|
<div class="scroll-table-content">
|
<tbody ref="boardTableBody">
|
<tr :class="(index + 1) % 2 === 0 ? 'evenTableTr' : 'oddTableTr'"
|
v-for="(item, index) in filteredAmountSalesData"
|
:key="item.period + item.area + index">
|
<td>{{ index + 1 }}</td>
|
<td>{{ item.productType }}</td>
|
<td>{{ item.period }}</td>
|
<td>{{ item.area }}</td>
|
<td>{{ item.sales }}</td>
|
</tr>
|
</tbody>
|
</div>
|
</table>
|
</div>
|
<div class="panel-summary-row">
|
<div class="summary-label">合计</div>
|
<div class="summary-value2">{{ filteredAmountSalesTotal }} 元</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
import {
|
ref,
|
computed,
|
onMounted,
|
onBeforeUnmount,
|
watch,
|
nextTick,
|
} from "vue";
|
import { useRouter } from "vue-router";
|
import * as echarts from "echarts";
|
import dayjs from "dayjs";
|
import PanelHeader from "@/views/reportAnalysis/PSIDataAnalysis/components/PanelHeader.vue";
|
import {
|
getDashboardStatistics,
|
getCustomerTrends,
|
getSalesAnalysisTrend,
|
getSalesAmountAnalysis,
|
} from "@/api/reportAnalysis/salesStatistics";
|
|
const router = useRouter();
|
const screenRoot = ref(null);
|
const isFullscreen = ref(false);
|
|
// 顶部栏时间(用于匹配BI大屏效果图)
|
const now = ref(dayjs());
|
const currentTime = computed(() => now.value.format("HH:mm:ss"));
|
const currentDateText = computed(() => {
|
const weekMap = {
|
0: "星期日",
|
1: "星期一",
|
2: "星期二",
|
3: "星期三",
|
4: "星期四",
|
5: "星期五",
|
6: "星期六",
|
};
|
return `${now.value.format("YYYY-MM-DD")} ${weekMap[now.value.day()] || ""}`;
|
});
|
let timeTicker = null;
|
|
const handleFullscreenChange = () => {
|
isFullscreen.value = !!document.fullscreenElement;
|
nextTick(() => {
|
handleResize();
|
});
|
};
|
|
const toggleFullscreen = async () => {
|
const rootEl = screenRoot.value;
|
if (!rootEl) return;
|
try {
|
if (!document.fullscreenElement) {
|
await rootEl.requestFullscreen();
|
} else {
|
await document.exitFullscreen();
|
}
|
} catch (error) {
|
console.error("全屏切换失败:", error);
|
}
|
};
|
|
// 筛选条件
|
const dateRange = ref([]);
|
const productType = ref("");
|
const salesArea = ref("");
|
const statDimension = ref("month");
|
|
// 图表引用
|
const salesVolumeChart = ref(null);
|
const salesAmountChart = ref(null);
|
const productTypeChart = ref(null);
|
const salesAreaChart = ref(null);
|
const productTypeTrendChart = ref(null);
|
const blockTableBody = ref(null);
|
const boardTableBody = ref(null);
|
|
// 选择器数据
|
// 销量分析趋势图
|
const chartTimeDimension = ref("年");
|
const chartTimeDimension2 = ref("年");
|
|
const chartSelectedArea = ref("全部");
|
const chartProductType = ref("砌块");
|
const chartProductType2 = ref("砌块");
|
|
// 销量数据统计
|
const tableTimeDimension = ref("年");
|
const tableSelectedArea = ref("全部");
|
const tableSelectedArea2 = ref("全部");
|
|
const tableProductType = ref("砌块");
|
const boardTimeDimension = ref("年");
|
const boardSelectedArea = ref("全部");
|
const boardProductType = ref("板材");
|
const customerTimeDimension = ref("年");
|
|
const salesAreas = [
|
"全部",
|
"A销售区",
|
"B销售区",
|
"C销售区",
|
"D销售区",
|
"E销售区",
|
];
|
|
// 表格数据
|
const blockSalesData = ref([
|
{ period: "2024-01", area: "***销售区", sales: 1250, sort: 1 },
|
{ period: "2024-02", area: "xxx销售区", sales: 1180, sort: 2 },
|
{ period: "2024-03", area: "xxx销售区", sales: 1050, sort: 3 },
|
{ period: "2024-04", area: "xxx销售区", sales: 980, sort: 4 },
|
{ period: "2024-05", area: "xxx销售区", sales: 920, sort: 5 },
|
{ period: "2024-06", area: "***销售区", sales: 880, sort: 6 },
|
{ period: "2024-07", area: "xxx销售区", sales: 850, sort: 7 },
|
{ period: "2024-08", area: "***销售区", sales: 820, sort: 8 },
|
{ period: "2024-09", area: "xxx销售区", sales: 790, sort: 9 },
|
{ period: "2024-10", area: "***销售区", sales: 750, sort: 10 },
|
]);
|
|
const boardSalesData = ref([
|
{ period: "2024-01", area: "***销售区", sales: 980, sort: 1 },
|
{ period: "2024-02", area: "xxx销售区", sales: 920, sort: 2 },
|
{ period: "2024-03", area: "***销售区", sales: 880, sort: 3 },
|
{ period: "2024-04", area: "xxx销售区", sales: 850, sort: 4 },
|
{ period: "2024-05", area: "xxx销售区", sales: 820, sort: 5 },
|
{ period: "2024-06", area: "***销售区", sales: 790, sort: 6 },
|
{ period: "2024-07", area: "xxx销售区", sales: 750, sort: 7 },
|
{ period: "2024-08", area: "xxx销售区", sales: 720, sort: 8 },
|
{ period: "2024-09", area: "***销售区", sales: 690, sort: 9 },
|
{ period: "2024-10", area: "xxx销售区", sales: 650, sort: 10 },
|
]);
|
const cumulativeSalesVolumeChart = ref(null);
|
const cumulativeSalesAmountChart = ref(null);
|
|
// 图表实例
|
let salesVolumeChartInstance = null;
|
let salesAmountChartInstance = null;
|
let productTypeChartInstance = null;
|
let salesAreaChartInstance = null;
|
let productTypeTrendChartInstance = null;
|
let cumulativeSalesVolumeChartInstance = null;
|
let cumulativeSalesAmountChartInstance = null;
|
|
// 模拟数据
|
const mockData = [
|
// 2026年1月数据
|
{
|
productType: "砌块",
|
salesArea: "华东",
|
period: "2026-01",
|
salesVolume: 1200,
|
salesAmount: 180,
|
newCustomers: 5,
|
totalCustomers: 120,
|
},
|
{
|
productType: "砌块",
|
salesArea: "华北",
|
period: "2026-01",
|
salesVolume: 800,
|
salesAmount: 120,
|
newCustomers: 3,
|
totalCustomers: 80,
|
},
|
{
|
productType: "砌块",
|
salesArea: "华南",
|
period: "2026-01",
|
salesVolume: 600,
|
salesAmount: 90,
|
newCustomers: 2,
|
totalCustomers: 60,
|
},
|
{
|
productType: "板材",
|
salesArea: "华东",
|
period: "2026-01",
|
salesVolume: 900,
|
salesAmount: 270,
|
newCustomers: 4,
|
totalCustomers: 100,
|
},
|
{
|
productType: "板材",
|
salesArea: "华北",
|
period: "2026-01",
|
salesVolume: 500,
|
salesAmount: 150,
|
newCustomers: 2,
|
totalCustomers: 70,
|
},
|
{
|
productType: "型材",
|
salesArea: "华东",
|
period: "2026-01",
|
salesVolume: 400,
|
salesAmount: 200,
|
newCustomers: 3,
|
totalCustomers: 50,
|
},
|
// 2026年2月数据
|
{
|
productType: "砌块",
|
salesArea: "华东",
|
period: "2026-02",
|
salesVolume: 1300,
|
salesAmount: 195,
|
newCustomers: 4,
|
totalCustomers: 124,
|
},
|
{
|
productType: "砌块",
|
salesArea: "华北",
|
period: "2026-02",
|
salesVolume: 850,
|
salesAmount: 127.5,
|
newCustomers: 2,
|
totalCustomers: 82,
|
},
|
{
|
productType: "砌块",
|
salesArea: "华南",
|
period: "2026-02",
|
salesVolume: 650,
|
salesAmount: 97.5,
|
newCustomers: 1,
|
totalCustomers: 61,
|
},
|
{
|
productType: "板材",
|
salesArea: "华东",
|
period: "2026-02",
|
salesVolume: 950,
|
salesAmount: 285,
|
newCustomers: 3,
|
totalCustomers: 103,
|
},
|
{
|
productType: "板材",
|
salesArea: "华北",
|
period: "2026-02",
|
salesVolume: 550,
|
salesAmount: 165,
|
newCustomers: 1,
|
totalCustomers: 71,
|
},
|
{
|
productType: "型材",
|
salesArea: "华东",
|
period: "2026-02",
|
salesVolume: 450,
|
salesAmount: 225,
|
newCustomers: 2,
|
totalCustomers: 52,
|
},
|
// 2026年3月数据
|
{
|
productType: "砌块",
|
salesArea: "华东",
|
period: "2026-03",
|
salesVolume: 1400,
|
salesAmount: 210,
|
newCustomers: 6,
|
totalCustomers: 130,
|
},
|
{
|
productType: "砌块",
|
salesArea: "华北",
|
period: "2026-03",
|
salesVolume: 900,
|
salesAmount: 135,
|
newCustomers: 3,
|
totalCustomers: 85,
|
},
|
{
|
productType: "砌块",
|
salesArea: "华南",
|
period: "2026-03",
|
salesVolume: 700,
|
salesAmount: 105,
|
newCustomers: 2,
|
totalCustomers: 63,
|
},
|
{
|
productType: "板材",
|
salesArea: "华东",
|
period: "2026-03",
|
salesVolume: 1000,
|
salesAmount: 300,
|
newCustomers: 5,
|
totalCustomers: 108,
|
},
|
{
|
productType: "板材",
|
salesArea: "华北",
|
period: "2026-03",
|
salesVolume: 600,
|
salesAmount: 180,
|
newCustomers: 2,
|
totalCustomers: 73,
|
},
|
{
|
productType: "型材",
|
salesArea: "华东",
|
period: "2026-03",
|
salesVolume: 500,
|
salesAmount: 250,
|
newCustomers: 3,
|
totalCustomers: 55,
|
},
|
// 西南和西北地区数据
|
{
|
productType: "砌块",
|
salesArea: "西南",
|
period: "2026-03",
|
salesVolume: 500,
|
salesAmount: 75,
|
newCustomers: 2,
|
totalCustomers: 40,
|
},
|
{
|
productType: "板材",
|
salesArea: "西南",
|
period: "2026-03",
|
salesVolume: 300,
|
salesAmount: 90,
|
newCustomers: 1,
|
totalCustomers: 30,
|
},
|
{
|
productType: "砌块",
|
salesArea: "西北",
|
period: "2026-03",
|
salesVolume: 400,
|
salesAmount: 60,
|
newCustomers: 1,
|
totalCustomers: 35,
|
},
|
{
|
productType: "板材",
|
salesArea: "西北",
|
period: "2026-03",
|
salesVolume: 200,
|
salesAmount: 60,
|
newCustomers: 1,
|
totalCustomers: 25,
|
},
|
];
|
|
// 计算属性
|
const filteredData = computed(() => {
|
let result = [...mockData];
|
|
// 按产品类型筛选
|
if (productType.value) {
|
result = result.filter(item => {
|
const typeMap = { block: "砌块", board: "板材", profile: "型材" };
|
return item.productType === typeMap[productType.value];
|
});
|
}
|
|
// 按销售区域筛选
|
if (salesArea.value) {
|
result = result.filter(item => {
|
const areaMap = {
|
east: "华东",
|
north: "华北",
|
south: "华南",
|
southwest: "西南",
|
northwest: "西北",
|
};
|
return item.salesArea === areaMap[salesArea.value];
|
});
|
}
|
|
// 按时间范围筛选
|
if (dateRange.value && dateRange.value.length === 2) {
|
const startDate = dayjs(dateRange.value[0]);
|
const endDate = dayjs(dateRange.value[1]);
|
|
result = result.filter(item => {
|
const itemDate = dayjs(item.period);
|
return (
|
itemDate.isAfter(startDate.subtract(1, "day")) &&
|
itemDate.isBefore(endDate.add(1, "day"))
|
);
|
});
|
}
|
|
return result;
|
});
|
|
// 核心指标计算
|
const totalSalesVolume = computed(() => {
|
return filteredData.value.reduce((sum, item) => sum + item.salesVolume, 0);
|
});
|
|
const totalSalesAmount = ref(1299);
|
const centerNewCustomerCount = ref(112);
|
const completedOrders = ref(1829);
|
const totalSalesAreaCount = ref(12);
|
|
const newCustomerCount = computed(() => {
|
return filteredData.value.reduce((sum, item) => sum + item.newCustomers, 0);
|
});
|
|
const totalCustomerCount = computed(() => {
|
// 计算每个区域和产品类型的最大客户数
|
const customerMap = {};
|
filteredData.value.forEach(item => {
|
const key = `${item.productType}-${item.salesArea}`;
|
if (!customerMap[key] || item.totalCustomers > customerMap[key]) {
|
customerMap[key] = item.totalCustomers;
|
}
|
});
|
return Object.values(customerMap).reduce((sum, count) => sum + count, 0);
|
});
|
|
// 客户趋势数据
|
const customerTrendsData = ref([]);
|
// 销量分析趋势数据
|
const salesAnalysisTrendData = ref([]);
|
// 销量数据统计表格数据
|
const tableSalesData = ref([]);
|
// 销量数据统计表格总计
|
const tableSalesTotal = ref(0);
|
// 动态销售区域列表
|
const tableSalesAreas = ref([]);
|
|
// 变化率计算(模拟)
|
const salesVolumeChange = ref("+5.2");
|
const salesAmountChange = ref("+7.8");
|
const customerCountChange = ref("+3.5");
|
const totalCustomerChange = ref("+2.1");
|
|
// 获取中心看板数据
|
const fetchDashboardData = async () => {
|
try {
|
const response = await getDashboardStatistics();
|
if (response && response.data) {
|
totalSalesAmount.value = response.data.price || 0;
|
completedOrders.value = response.data.delivery || 0;
|
centerNewCustomerCount.value = response.data.customer || 0;
|
totalSalesAreaCount.value = response.data.volume || 0;
|
}
|
} catch (error) {
|
console.error("获取中心看板数据失败:", error);
|
}
|
};
|
|
// 获取客户趋势数据
|
const fetchCustomerTrendsData = async () => {
|
try {
|
const response = await getCustomerTrends({
|
days: customerTimeDimension.value,
|
});
|
if (response && response.data) {
|
// API返回的数据结构如下:
|
// {
|
// "dates": ["2026-01-01", "2025-01-01", ...],
|
// "customerTrends": [{"ALLIN": 4, "银川": 3, ...}, ...]
|
// }
|
customerTrendsData.value = response.data;
|
updateCharts();
|
}
|
} catch (error) {
|
console.error("获取客户趋势数据失败:", error);
|
}
|
};
|
|
// 获取销量分析趋势数据
|
const fetchSalesAnalysisTrendData = async () => {
|
try {
|
const response = await getSalesAnalysisTrend({
|
type: chartProductType.value, // 砌块或板材
|
days: chartTimeDimension.value, // 年或月
|
});
|
if (response && response.data) {
|
// API返回的数据结构如下:
|
// {
|
// "dates": ["2026-01-01", "2025-01-01", ...],
|
// "customerTrends": [{"内蒙古": 470, "银川": 3600, ...}, ...]
|
// }
|
salesAnalysisTrendData.value = response.data;
|
updateCharts();
|
}
|
} catch (error) {
|
console.error("获取销量分析趋势数据失败:", error);
|
}
|
};
|
|
// 获取销量数据统计表格数据
|
const fetchTableSalesData = async () => {
|
try {
|
const response = await getSalesAnalysisTrend({
|
type: tableProductType.value, // 砌块或板材
|
days: tableTimeDimension.value, // 年或月
|
});
|
if (response && response.data) {
|
// API返回的数据结构如下:
|
// {
|
// "dates": ["2026-01-01", "2025-01-01", ...],
|
// "customerTrends": [{"内蒙古": 470, "银川": 3600, ...}, ...]
|
// }
|
updateTableSalesData(response.data);
|
}
|
} catch (error) {
|
console.error("获取销量数据统计表格数据失败:", error);
|
}
|
};
|
|
// 更新销量数据统计表格
|
const updateTableSalesData = data => {
|
if (!data || !data.dates || !data.customerTrends) {
|
return;
|
}
|
console.log(data, "datas");
|
const dates = data.dates;
|
const customerTrends = data.customerTrends;
|
const tableData = [];
|
let total = 0;
|
const areaSet = new Set();
|
|
dates.forEach((date, index) => {
|
const trend = customerTrends[index];
|
if (trend) {
|
// 提取所有销售区域
|
Object.keys(trend).forEach(area => {
|
if (area !== "全部") {
|
areaSet.add(area);
|
const sales = trend[area] || 0;
|
tableData.push({
|
period: date,
|
area: area,
|
productType: tableProductType.value,
|
sales: sales,
|
sort: tableData.length + 1,
|
});
|
total += sales;
|
}
|
});
|
}
|
});
|
|
// 更新销售区域列表,添加"全部"选项
|
tableSalesAreas.value = ["全部", ...Array.from(areaSet)];
|
// 确保 tableSelectedArea 在销售区域列表中
|
if (
|
tableSalesAreas.value.length > 0 &&
|
!tableSalesAreas.value.includes(tableSelectedArea.value)
|
) {
|
tableSelectedArea.value = "全部";
|
}
|
|
console.log(tableData);
|
tableSalesData.value = tableData;
|
tableSalesTotal.value = total;
|
};
|
|
// 处理图表时间维度变化
|
const handleChartTimeDimensionChange = dimension => {
|
chartTimeDimension.value = dimension;
|
fetchSalesAnalysisTrendData();
|
};
|
|
// 处理图表产品类型变化
|
const handleChartProductTypeChange = type => {
|
chartProductType.value = type;
|
fetchSalesAnalysisTrendData();
|
};
|
|
// 处理表格时间维度变化
|
const handleTableTimeDimensionChange = dimension => {
|
tableTimeDimension.value = dimension;
|
fetchTableSalesData();
|
// 重新启动滚动,根据时间维度决定是否滚动
|
startBlockTableScroll();
|
};
|
|
// 处理表格产品类型变化
|
const handleTableProductTypeChange = type => {
|
tableProductType.value = type;
|
fetchTableSalesData();
|
};
|
|
// 处理表格销售区域变化
|
const handleTableAreaChange = area => {
|
tableSelectedArea.value = area;
|
};
|
|
// 表格数据
|
const tableData = computed(() => {
|
return filteredData.value.map(item => {
|
// 计算累计值(模拟)
|
const cumulativeSalesVolume = item.salesVolume * 1.5;
|
const cumulativeSalesAmount = item.salesAmount * 1.5;
|
const cumulativeNewCustomers = item.newCustomers * 2;
|
|
return {
|
...item,
|
cumulativeSalesVolume,
|
cumulativeSalesAmount,
|
cumulativeNewCustomers,
|
};
|
});
|
});
|
|
// 筛选后的表格数据
|
const filteredTableSalesData = computed(() => {
|
if (tableSelectedArea.value === "全部") {
|
// 按年月分组汇总数据
|
const groupedData = {};
|
tableSalesData.value.forEach(item => {
|
const key = item.period;
|
if (!groupedData[key]) {
|
groupedData[key] = {
|
period: item.period,
|
area: "全部",
|
productType: item.productType,
|
sales: 0,
|
sort: 0,
|
};
|
}
|
groupedData[key].sales += item.sales;
|
});
|
// 转换为数组并按年月排序
|
return Object.values(groupedData).sort((a, b) => {
|
return new Date(b.period) - new Date(a.period);
|
});
|
} else {
|
return tableSalesData.value.filter(
|
item => item.area === tableSelectedArea.value
|
);
|
}
|
});
|
|
// 筛选后的表格数据总计
|
const filteredTableSalesTotal = computed(() => {
|
return filteredTableSalesData.value.reduce(
|
(total, item) => total + item.sales,
|
0
|
);
|
});
|
|
// 销量趋势图表配置
|
const salesVolumeChartOption = computed(() => {
|
const colors = [
|
"#00A4ED",
|
"#34D8F7",
|
"#4A8BFF",
|
"#8A6BFF",
|
"#C8C447",
|
"#FF6B6B",
|
"#FF9500",
|
"#4CD964",
|
"#5AC8FA",
|
];
|
const periodType = blockTimeDimension.value;
|
|
// 生成时间段
|
let periods = [];
|
let salesAreas = [];
|
let series = [];
|
|
if (
|
salesAnalysisTrendData.value &&
|
salesAnalysisTrendData.value.dates &&
|
salesAnalysisTrendData.value.customerTrends
|
) {
|
// 使用API返回的日期
|
periods = salesAnalysisTrendData.value.dates;
|
// 提取销售区域
|
const customerTrends = salesAnalysisTrendData.value.customerTrends;
|
if (customerTrends.length > 0) {
|
// 提取销售区域并确保"全部"在第一个位置
|
const allAreas = Object.keys(customerTrends[0]);
|
salesAreas = allAreas.sort((a, b) => {
|
if (a === "全部") return -1;
|
if (b === "全部") return 1;
|
return 0;
|
});
|
// 为每个销售区生成数据
|
series = salesAreas.map((area, index) => {
|
const data = customerTrends.map(trend => trend[area] || 0);
|
return {
|
name: area,
|
data: data,
|
type: "line",
|
smooth: false,
|
// symbolSize: getResponsiveValue(8),
|
lineStyle: {
|
width: getResponsiveValue(1),
|
color: colors[index % colors.length],
|
},
|
itemStyle: { color: colors[index % colors.length] },
|
areaStyle: {
|
opacity: 0.4,
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: colors[index % colors.length] + "80" },
|
{ offset: 1, color: colors[index % colors.length] + "00" },
|
]),
|
},
|
};
|
});
|
}
|
} else {
|
// 模拟数据
|
salesAreas = [
|
"全部",
|
"A销售区",
|
"B销售区",
|
"C销售区",
|
"D销售区",
|
"E销售区",
|
];
|
const year = 2024;
|
if (periodType === "year") {
|
// 年度数据:12个月
|
for (let month = 1; month <= 12; month++) {
|
periods.push(`${year}-${month.toString().padStart(2, "0")}`);
|
}
|
} else {
|
// 月度数据:30天
|
const month = 1;
|
for (let day = 1; day <= 30; day++) {
|
periods.push(
|
`${year}-${month.toString().padStart(2, "0")}-${day
|
.toString()
|
.padStart(2, "0")}`
|
);
|
}
|
}
|
// 为每个销售区生成数据
|
series = salesAreas.map((area, index) => {
|
const data = periods.map(() => {
|
return periodType === "year"
|
? Math.floor(Math.random() * 500) + 800
|
: Math.floor(Math.random() * 50) + 20;
|
});
|
|
return {
|
name: area,
|
data: data,
|
type: "line",
|
smooth: false,
|
// symbolSize: getResponsiveValue(8),
|
lineStyle: { width: getResponsiveValue(1), color: colors[index] },
|
itemStyle: { color: colors[index] },
|
areaStyle: {
|
opacity: 0.4,
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: colors[index] + "80" },
|
{ offset: 1, color: colors[index] + "00" },
|
]),
|
},
|
};
|
});
|
}
|
|
return {
|
backgroundColor: "transparent",
|
tooltip: {
|
trigger: "axis",
|
backgroundColor: "rgba(0,0,0,0.55)",
|
borderColor: "rgba(64,158,255,0.25)",
|
borderWidth: getResponsiveValue(1),
|
textStyle: { color: "#B8C8E0", fontSize: getResponsiveValue(11) },
|
formatter: function (params) {
|
let result = params[0].name + "<br/>";
|
params.forEach(param => {
|
result += `${param.marker}${param.seriesName}: ${param.value} 立方米<br/>`;
|
});
|
return result;
|
},
|
},
|
legend: {
|
data: salesAreas,
|
top: "10%",
|
right: "1%",
|
textStyle: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(10),
|
},
|
itemWidth: getResponsiveValue(10),
|
itemHeight: getResponsiveValue(10),
|
},
|
grid: {
|
left: "1%",
|
right: "1%",
|
bottom: "1%",
|
top: "28%",
|
containLabel: true,
|
},
|
xAxis: {
|
type: "category",
|
data: periods,
|
axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } },
|
axisTick: { show: false },
|
axisLabel: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(11),
|
margin: getResponsiveValue(10),
|
},
|
splitLine: { show: false },
|
},
|
yAxis: {
|
type: "value",
|
name: "",
|
axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } },
|
axisLabel: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(11),
|
margin: getResponsiveValue(8),
|
},
|
splitLine: { lineStyle: { color: "rgba(184,200,224,0.12)" } },
|
},
|
series: series,
|
};
|
});
|
|
// 销售金额趋势图表配置
|
const salesAmountChartOption = computed(() => {
|
const { dates = [], customerTrends = [] } = salesAmountChartData.value;
|
|
// 提取所有销售区域
|
const areaSet = new Set();
|
customerTrends.forEach(item => {
|
Object.keys(item).forEach(key => areaSet.add(key));
|
});
|
// 确保"全部"在第一个位置
|
const salesAreas = Array.from(areaSet).sort((a, b) => {
|
if (a === "全部") return -1;
|
if (b === "全部") return 1;
|
return 0;
|
});
|
|
const colors = [
|
"#00A4ED",
|
"#34D8F7",
|
"#4A8BFF",
|
"#8A6BFF",
|
"#C8C447",
|
"#FF6B6B",
|
];
|
|
// 为每个销售区生成数据
|
const series = salesAreas.map((area, index) => {
|
const data = customerTrends.map(item => item[area] || 0);
|
|
return {
|
name: area,
|
data: data,
|
type: "bar",
|
smooth: true,
|
lineStyle: {
|
width: getResponsiveValue(3),
|
color: colors[index % colors.length],
|
},
|
itemStyle: { color: colors[index % colors.length] },
|
areaStyle: {
|
opacity: 0.2,
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: colors[index % colors.length] + "80" },
|
{ offset: 1, color: colors[index % colors.length] + "00" },
|
]),
|
},
|
};
|
});
|
|
return {
|
backgroundColor: "transparent",
|
tooltip: {
|
trigger: "axis",
|
backgroundColor: "rgba(0,0,0,0.55)",
|
borderColor: "rgba(64,158,255,0.25)",
|
borderWidth: getResponsiveValue(1),
|
textStyle: { color: "#B8C8E0", fontSize: getResponsiveValue(11) },
|
formatter: function (params) {
|
let result = params[0]?.name + "<br/>" || "";
|
params.forEach(param => {
|
result += `${param.marker}${param.seriesName}: ${param.value} 元<br/>`;
|
});
|
return result;
|
},
|
},
|
legend: {
|
data: salesAreas,
|
top: "10%",
|
right: "1%",
|
textStyle: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(10),
|
},
|
itemWidth: getResponsiveValue(10),
|
itemHeight: getResponsiveValue(10),
|
},
|
grid: {
|
left: "1%",
|
right: "1%",
|
bottom: "1%",
|
top: "28%",
|
containLabel: true,
|
},
|
xAxis: {
|
type: "category",
|
data: dates,
|
axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } },
|
axisTick: { show: false },
|
axisLabel: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(11),
|
margin: getResponsiveValue(10),
|
},
|
splitLine: { show: false },
|
},
|
yAxis: {
|
type: "value",
|
name: "",
|
axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } },
|
axisLabel: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(11),
|
margin: getResponsiveValue(8),
|
},
|
splitLine: { lineStyle: { color: "rgba(184,200,224,0.12)" } },
|
},
|
series: series,
|
};
|
});
|
|
// 产品类型销量图表配置(横向柱状)
|
const productTypeChartOption = computed(() => {
|
const types = ["客户BB", "客户AA", "客户CC", "客户DD", "客户DD", "客户DD"];
|
const values = [130, 120, 102, 90, 90, 70];
|
const barColors = [
|
"#34D8F7",
|
"#4A8BFF",
|
"#8A6BFF",
|
"#C8C447",
|
"#C8C447",
|
"#C8C447",
|
];
|
|
return {
|
backgroundColor: "transparent",
|
tooltip: {
|
trigger: "axis",
|
axisPointer: { type: "shadow" },
|
backgroundColor: "rgba(0,0,0,0.55)",
|
borderColor: "rgba(64,158,255,0.25)",
|
borderWidth: getResponsiveValue(1),
|
textStyle: { color: "#B8C8E0", fontSize: getResponsiveValue(11) },
|
formatter: "{b}: {c} 立方米",
|
},
|
grid: {
|
left: "14%",
|
right: "6%",
|
top: "16%",
|
bottom: "8%",
|
containLabel: true,
|
},
|
xAxis: {
|
type: "value",
|
axisLine: { show: false },
|
axisLabel: { color: "#B8C8E0", fontSize: getResponsiveValue(11) },
|
splitLine: { lineStyle: { color: "rgba(184,200,224,0.12)" } },
|
},
|
yAxis: {
|
type: "category",
|
data: types,
|
axisTick: { show: false },
|
axisLine: { show: false },
|
axisLabel: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(11),
|
margin: getResponsiveValue(8),
|
},
|
},
|
series: [
|
{
|
name: "销量(立方米)",
|
type: "bar",
|
barWidth: getResponsiveValue(14),
|
data: values,
|
itemStyle: {
|
color: params => barColors[params.dataIndex] || "#00A4ED",
|
borderRadius: [
|
getResponsiveValue(6),
|
getResponsiveValue(6),
|
getResponsiveValue(6),
|
getResponsiveValue(6),
|
],
|
},
|
label: {
|
show: false,
|
},
|
},
|
],
|
};
|
});
|
|
// 销售区域销量图表配置(横向柱状)
|
const salesAreaChartOption = computed(() => {
|
const areas = ["客户BB", "客户AA", "客户CC", "客户DD", "客户DD", "客户DD"];
|
const values = [130, 120, 102, 90, 90, 70];
|
const barColors = [
|
"#34D8F7",
|
"#4A8BFF",
|
"#8A6BFF",
|
"#C8C447",
|
"#C8C447",
|
"#C8C447",
|
];
|
|
return {
|
backgroundColor: "transparent",
|
tooltip: {
|
trigger: "axis",
|
axisPointer: { type: "shadow" },
|
backgroundColor: "rgba(0,0,0,0.55)",
|
borderColor: "rgba(64,158,255,0.25)",
|
borderWidth: getResponsiveValue(1),
|
textStyle: { color: "#B8C8E0", fontSize: getResponsiveValue(11) },
|
formatter: "{b}: {c} 立方米",
|
},
|
grid: {
|
left: "14%",
|
right: "6%",
|
top: "16%",
|
bottom: "8%",
|
containLabel: true,
|
},
|
xAxis: {
|
type: "value",
|
axisLine: { show: false },
|
axisLabel: { color: "#B8C8E0", fontSize: getResponsiveValue(11) },
|
splitLine: { lineStyle: { color: "rgba(184,200,224,0.12)" } },
|
},
|
yAxis: {
|
type: "category",
|
data: areas,
|
axisTick: { show: false },
|
axisLine: { show: false },
|
axisLabel: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(11),
|
margin: getResponsiveValue(8),
|
},
|
},
|
series: [
|
{
|
name: "销量(立方米)",
|
type: "bar",
|
barWidth: getResponsiveValue(14),
|
data: values,
|
itemStyle: {
|
color: params => barColors[params.dataIndex] || "#00A4ED",
|
borderRadius: [
|
getResponsiveValue(6),
|
getResponsiveValue(6),
|
getResponsiveValue(6),
|
getResponsiveValue(6),
|
],
|
},
|
},
|
],
|
};
|
});
|
|
// 新增客户趋势图表配置(按销售区和年月维度)
|
const productTypeTrendChartOption = computed(() => {
|
const colors = [
|
"#00A4ED",
|
"#34D8F7",
|
"#4A8BFF",
|
"#8A6BFF",
|
"#C8C447",
|
"#FF6B6B",
|
"#FF8B6B",
|
"#FFCB6B",
|
"#8BC34A",
|
"#4CAF50",
|
];
|
const periodType = customerTimeDimension.value;
|
|
// 生成时间段
|
let periods = [];
|
let salesAreas = [];
|
let series = [];
|
|
if (
|
customerTrendsData.value &&
|
customerTrendsData.value.dates &&
|
customerTrendsData.value.customerTrends
|
) {
|
// 使用API返回的数据
|
periods = customerTrendsData.value.dates;
|
|
// 提取所有销售区域
|
const areaSet = new Set();
|
customerTrendsData.value.customerTrends.forEach(item => {
|
Object.keys(item).forEach(key => {
|
areaSet.add(key);
|
});
|
});
|
// 确保"全部"在第一个位置
|
salesAreas = Array.from(areaSet).sort((a, b) => {
|
if (a === "全部") return -1;
|
if (b === "全部") return 1;
|
return 0;
|
});
|
|
// 为每个销售区域生成数据
|
series = salesAreas.map((area, index) => {
|
const data = customerTrendsData.value.customerTrends.map((item, i) => {
|
return item[area] || 0;
|
});
|
|
return {
|
name: area,
|
data: data,
|
type: "line",
|
smooth: false,
|
lineStyle: {
|
width: getResponsiveValue(1),
|
color: colors[index % colors.length],
|
},
|
itemStyle: { color: colors[index % colors.length] },
|
};
|
});
|
} else {
|
// 模拟数据
|
const year = 2024;
|
if (periodType === "year") {
|
// 年度数据:12个月
|
for (let month = 1; month <= 12; month++) {
|
periods.push(`${year}-${month.toString().padStart(2, "0")}`);
|
}
|
} else {
|
// 月度数据:30天
|
const month = 1;
|
for (let day = 1; day <= 30; day++) {
|
periods.push(
|
`${year}-${month.toString().padStart(2, "0")}-${day
|
.toString()
|
.padStart(2, "0")}`
|
);
|
}
|
}
|
|
salesAreas = [];
|
series = salesAreas.map((area, index) => {
|
const data = periods.map(() => {
|
return periodType === "year"
|
? Math.floor(Math.random() * 10) + 2
|
: Math.floor(Math.random() * 3) + 1;
|
});
|
|
return {
|
name: area,
|
data: data,
|
type: "line",
|
smooth: false,
|
lineStyle: { width: getResponsiveValue(1), color: colors[index] },
|
itemStyle: { color: colors[index] },
|
};
|
});
|
}
|
|
return {
|
backgroundColor: "transparent",
|
tooltip: {
|
trigger: "axis",
|
backgroundColor: "rgba(0,0,0,0.55)",
|
borderColor: "rgba(64,158,255,0.25)",
|
borderWidth: getResponsiveValue(1),
|
textStyle: { color: "#B8C8E0", fontSize: getResponsiveValue(11) },
|
formatter: function (params) {
|
let result = params[0].name + "<br/>";
|
params.forEach(param => {
|
result += `${param.marker}${param.seriesName}: ${param.value} 家<br/>`;
|
});
|
return result;
|
},
|
},
|
legend: {
|
data: salesAreas,
|
top: "10%",
|
right: "1%",
|
textStyle: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(10),
|
},
|
itemWidth: getResponsiveValue(10),
|
itemHeight: getResponsiveValue(10),
|
},
|
grid: {
|
left: "1%",
|
right: "1%",
|
bottom: "1%",
|
top: "28%",
|
containLabel: true,
|
},
|
xAxis: {
|
type: "category",
|
data: periods,
|
axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } },
|
axisTick: { show: false },
|
axisLabel: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(11),
|
margin: getResponsiveValue(10),
|
},
|
splitLine: { show: false },
|
},
|
yAxis: {
|
type: "value",
|
name: "",
|
axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } },
|
axisLabel: {
|
color: "#B8C8E0",
|
fontSize: getResponsiveValue(11),
|
margin: getResponsiveValue(8),
|
},
|
splitLine: { lineStyle: { color: "rgba(184,200,224,0.12)" } },
|
},
|
series: series,
|
};
|
});
|
|
const baseWidth = ref(1650);
|
// 计算响应式值
|
const getResponsiveValue = baseValue => {
|
return Math.round((baseValue * window.innerWidth) / baseWidth.value);
|
};
|
|
// 初始化图表
|
const initCharts = () => {
|
// 初始化销量趋势图表
|
if (salesVolumeChart.value && !salesVolumeChartInstance) {
|
salesVolumeChartInstance = echarts.init(salesVolumeChart.value);
|
}
|
|
// 初始化销售金额趋势图表
|
if (salesAmountChart.value && !salesAmountChartInstance) {
|
salesAmountChartInstance = echarts.init(salesAmountChart.value);
|
}
|
|
// 初始化产品类型分布图表
|
if (productTypeChart.value && !productTypeChartInstance) {
|
productTypeChartInstance = echarts.init(productTypeChart.value);
|
}
|
|
// 初始化销售区域分布图表
|
if (salesAreaChart.value && !salesAreaChartInstance) {
|
salesAreaChartInstance = echarts.init(salesAreaChart.value);
|
}
|
|
// 初始化新增客户趋势图表
|
if (productTypeTrendChart.value && !productTypeTrendChartInstance) {
|
productTypeTrendChartInstance = echarts.init(productTypeTrendChart.value);
|
}
|
|
// 初始化累计销量趋势图表
|
if (cumulativeSalesVolumeChart.value && !cumulativeSalesVolumeChartInstance) {
|
cumulativeSalesVolumeChartInstance = echarts.init(
|
cumulativeSalesVolumeChart.value
|
);
|
}
|
|
// 初始化累计销售金额趋势图表
|
if (cumulativeSalesAmountChart.value && !cumulativeSalesAmountChartInstance) {
|
cumulativeSalesAmountChartInstance = echarts.init(
|
cumulativeSalesAmountChart.value
|
);
|
}
|
|
updateCharts();
|
};
|
// 更新图表
|
const updateCharts = () => {
|
// 更新销量趋势图表
|
if (salesVolumeChartInstance) {
|
salesVolumeChartInstance.setOption(
|
JSON.parse(JSON.stringify(salesVolumeChartOption.value))
|
);
|
}
|
|
// 更新销售金额趋势图表
|
if (salesAmountChartInstance) {
|
salesAmountChartInstance.setOption(salesAmountChartOption.value);
|
}
|
|
// 更新产品类型分布图表
|
if (productTypeChartInstance) {
|
productTypeChartInstance.setOption(productTypeChartOption.value);
|
}
|
|
// 更新销售区域分布图表
|
if (salesAreaChartInstance) {
|
salesAreaChartInstance.setOption(salesAreaChartOption.value);
|
}
|
|
// 更新新增客户趋势图表
|
if (productTypeTrendChartInstance) {
|
productTypeTrendChartInstance.setOption(productTypeTrendChartOption.value);
|
}
|
|
// 更新累计销量趋势图表
|
if (cumulativeSalesVolumeChartInstance) {
|
cumulativeSalesVolumeChartInstance.setOption(
|
cumulativeSalesVolumeChartOption.value
|
);
|
}
|
|
// 更新累计销售金额趋势图表
|
if (cumulativeSalesAmountChartInstance) {
|
cumulativeSalesAmountChartInstance.setOption(
|
cumulativeSalesAmountChartOption.value
|
);
|
}
|
};
|
|
// 表格动画控制
|
let blockScrollTimer = null;
|
let boardScrollTimer = null;
|
let blockCurrentIndex = 0;
|
let boardCurrentIndex = 0;
|
const startBlockTableScroll = () => {
|
if (blockScrollTimer) {
|
clearInterval(blockScrollTimer);
|
}
|
|
// 只有当时间维度不是"年"时才启动滚动
|
if (tableTimeDimension.value === "年") {
|
return;
|
}
|
|
const scrollTable = () => {
|
if (!blockTableBody.value) return;
|
const rows = blockTableBody.value.querySelectorAll("tr");
|
if (rows.length === 0) return;
|
|
const rowHeight = rows[0].offsetHeight;
|
|
blockTableBody.value.style.transition = "transform 0.5s ease-in-out";
|
blockTableBody.value.style.transform = `translateY(-${rowHeight}px)`;
|
|
setTimeout(() => {
|
blockTableBody.value.style.transition = "none";
|
blockTableBody.value.style.transform = "translateY(0)";
|
|
// 直接操作DOM,将第一行移到最后
|
const firstRow = rows[0];
|
blockTableBody.value.removeChild(firstRow);
|
blockTableBody.value.appendChild(firstRow);
|
}, 500);
|
};
|
|
blockScrollTimer = setInterval(scrollTable, 2000);
|
};
|
|
const startBoardTableScroll = () => {
|
if (boardScrollTimer) {
|
clearInterval(boardScrollTimer);
|
}
|
|
const scrollTable = () => {
|
if (!boardTableBody.value || boardSalesData.value.length === 0) return;
|
|
const rows = boardTableBody.value.querySelectorAll("tr");
|
if (rows.length === 0) return;
|
|
const rowHeight = rows[0].offsetHeight;
|
|
boardTableBody.value.style.transition = "transform 0.5s ease-in-out";
|
boardTableBody.value.style.transform = `translateY(-${rowHeight}px)`;
|
|
setTimeout(() => {
|
boardTableBody.value.style.transition = "none";
|
boardTableBody.value.style.transform = "translateY(0)";
|
|
const firstItem = boardSalesData.value[0];
|
boardSalesData.value.shift();
|
boardSalesData.value.push(firstItem);
|
}, 500);
|
};
|
|
boardScrollTimer = setInterval(scrollTable, 2000);
|
};
|
|
const stopTableScroll = () => {
|
if (blockScrollTimer) {
|
clearInterval(blockScrollTimer);
|
blockScrollTimer = null;
|
}
|
if (boardScrollTimer) {
|
clearInterval(boardScrollTimer);
|
boardScrollTimer = null;
|
}
|
if (amountScrollTimer.value) {
|
clearInterval(amountScrollTimer.value);
|
amountScrollTimer.value = null;
|
}
|
};
|
|
// 处理时间维度选择
|
const handleBlockTimeDimensionChange = dimension => {
|
blockTimeDimension.value = dimension;
|
generateBlockSalesData();
|
};
|
|
const handleBoardTimeDimensionChange = dimension => {
|
boardTimeDimension.value = dimension;
|
generateBoardSalesData();
|
};
|
const blockProductType = ref("砌块");
|
// 处理产品类型选择
|
const handleBlockProductTypeChange = type => {
|
blockProductType.value = type;
|
generateBlockSalesData();
|
};
|
|
const handleBoardProductTypeChange = type => {
|
boardProductType.value = type;
|
generateBoardSalesData();
|
};
|
const blockSelectedArea = ref("全部");
|
// 处理销售区选择
|
const handleBlockAreaChange = area => {
|
blockSelectedArea.value = area;
|
generateBlockSalesData();
|
};
|
|
const handleBoardAreaChange = area => {
|
boardSelectedArea.value = area;
|
generateBoardSalesData();
|
};
|
|
// 处理新增客户趋势时间维度切换
|
const handleCustomerTimeDimensionChange = dimension => {
|
customerTimeDimension.value = dimension;
|
fetchCustomerTrendsData();
|
};
|
const blockTimeDimension = ref("年");
|
|
// 生成砌块销售数据
|
const generateBlockSalesData = () => {
|
const data = [];
|
const year = 2024;
|
|
if (blockTimeDimension.value === "year") {
|
// 年度数据:12个月
|
for (let month = 1; month <= 12; month++) {
|
const period = `${year}-${month.toString().padStart(2, "0")}`;
|
data.push({
|
period: period,
|
area: blockSelectedArea.value,
|
productType: blockProductType.value,
|
sales: Math.floor(Math.random() * 500) + 800,
|
sort: month,
|
});
|
}
|
} else {
|
// 月度数据:30天
|
const month = 1;
|
for (let day = 1; day <= 30; day++) {
|
const period = `${year}-${month.toString().padStart(2, "0")}-${day
|
.toString()
|
.padStart(2, "0")}`;
|
data.push({
|
period: period,
|
area: blockSelectedArea.value,
|
productType: blockProductType.value,
|
sales: Math.floor(Math.random() * 50) + 20,
|
sort: day,
|
});
|
}
|
}
|
|
blockSalesData.value = data;
|
// 更新图表
|
updateCharts();
|
};
|
|
// 生成板材销售数据
|
const generateBoardSalesData = () => {
|
const data = [];
|
const year = 2024;
|
|
if (boardTimeDimension.value === "year") {
|
// 年度数据:12个月
|
for (let month = 1; month <= 12; month++) {
|
const period = `${year}-${month.toString().padStart(2, "0")}`;
|
data.push({
|
period: period,
|
area: boardSelectedArea.value,
|
productType: boardProductType.value,
|
sales: Math.floor(Math.random() * 400) + 600,
|
sort: month,
|
});
|
}
|
} else {
|
// 月度数据:30天
|
const month = 1;
|
for (let day = 1; day <= 30; day++) {
|
const period = `${year}-${month.toString().padStart(2, "0")}-${day
|
.toString()
|
.padStart(2, "0")}`;
|
data.push({
|
period: period,
|
area: boardSelectedArea.value,
|
productType: boardProductType.value,
|
sales: Math.floor(Math.random() * 40) + 15,
|
sort: day,
|
});
|
}
|
}
|
|
boardSalesData.value = data;
|
// 更新图表
|
updateCharts();
|
};
|
|
// 监听窗口大小变化
|
const handleResize = () => {
|
console.log("resize");
|
// 先更新图表选项,重新计算响应式值
|
updateCharts();
|
// 然后调整图表大小
|
if (salesVolumeChartInstance) {
|
salesVolumeChartInstance.resize();
|
}
|
if (salesAmountChartInstance) {
|
salesAmountChartInstance.resize();
|
}
|
if (productTypeChartInstance) {
|
productTypeChartInstance.resize();
|
}
|
if (salesAreaChartInstance) {
|
salesAreaChartInstance.resize();
|
}
|
if (productTypeTrendChartInstance) {
|
productTypeTrendChartInstance.resize();
|
}
|
if (cumulativeSalesVolumeChartInstance) {
|
cumulativeSalesVolumeChartInstance.resize();
|
}
|
if (cumulativeSalesAmountChartInstance) {
|
cumulativeSalesAmountChartInstance.resize();
|
}
|
};
|
|
// 生命周期
|
onMounted(async () => {
|
// 启动顶部栏时间刷新
|
if (!timeTicker) {
|
timeTicker = setInterval(() => {
|
now.value = dayjs();
|
}, 1000);
|
}
|
|
// 设置默认日期范围为最近3个月
|
const endDate = dayjs();
|
const startDate = endDate.subtract(3, "month");
|
dateRange.value = [
|
startDate.format("YYYY-MM-DD"),
|
endDate.format("YYYY-MM-DD"),
|
];
|
|
// 生成初始数据
|
generateBlockSalesData();
|
generateBoardSalesData();
|
|
// 获取数据
|
await fetchDashboardData();
|
await fetchCustomerTrendsData();
|
await fetchSalesAnalysisTrendData();
|
await fetchTableSalesData();
|
await fetchSalesAmountChartData();
|
await fetchSalesAmountTableData();
|
|
// 等待DOM更新后初始化图表
|
nextTick(() => {
|
initCharts();
|
// 启动表格滚动动画
|
startBlockTableScroll();
|
startAmountTableScroll();
|
});
|
|
// 添加窗口大小变化监听
|
window.addEventListener("resize", handleResize);
|
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
});
|
|
// 监听图表时间维度和产品类型变化
|
watch([chartTimeDimension, chartProductType], async () => {
|
await fetchSalesAnalysisTrendData();
|
});
|
|
// 监听表格时间维度和产品类型变化
|
watch([tableTimeDimension, tableProductType], async () => {
|
await fetchTableSalesData();
|
});
|
|
// 销售金额分析图表数据(右上)
|
const salesAmountChartData = ref({
|
dates: [],
|
customerTrends: [],
|
});
|
|
// 销售额数据统计表格数据(右下)
|
const salesAmountTableData = ref({
|
dates: [],
|
customerTrends: [],
|
});
|
|
// 销售额数据统计表格筛选状态(右下)
|
const tableTimeDimension2 = ref("年");
|
const tableProductType2 = ref("砌块");
|
|
// 销售额数据统计表格数据
|
const amountSalesData = ref([]);
|
const amountScrollTimer = ref(null);
|
|
// 获取销售金额分析图表数据(右上)
|
const fetchSalesAmountChartData = async () => {
|
try {
|
const response = await getSalesAmountAnalysis({
|
type: chartProductType2.value,
|
days: chartTimeDimension2.value,
|
});
|
if (response?.data) {
|
salesAmountChartData.value = response.data;
|
updateCharts();
|
}
|
} catch (error) {
|
console.error("获取销售金额分析图表数据失败:", error);
|
// 使用模拟数据
|
salesAmountChartData.value = {
|
dates: [
|
"2026-01-01",
|
"2025-01-01",
|
"2024-01-01",
|
"2023-01-01",
|
"2022-01-01",
|
],
|
customerTrends: [
|
{ 内蒙古: 100, 银川: 200, 自提: 300, 其他: 150, 全部: 750 },
|
{ 内蒙古: 80, 银川: 180, 自提: 280, 其他: 130, 全部: 670 },
|
{ 内蒙古: 90, 银川: 190, 自提: 290, 其他: 140, 全部: 710 },
|
{ 内蒙古: 70, 银川: 170, 自提: 270, 其他: 120, 全部: 630 },
|
{ 内蒙古: 110, 银川: 210, 自提: 310, 其他: 160, 全部: 790 },
|
],
|
};
|
}
|
};
|
|
// 获取销售额数据统计表格数据(右下)
|
const fetchSalesAmountTableData = async () => {
|
try {
|
const response = await getSalesAmountAnalysis({
|
type: tableProductType2.value,
|
days: tableTimeDimension2.value,
|
});
|
if (response?.data) {
|
salesAmountTableData.value = response.data;
|
updateAmountSalesData();
|
}
|
} catch (error) {
|
console.error("获取销售额数据统计表格数据失败:", error);
|
// 使用模拟数据
|
salesAmountTableData.value = {
|
dates: [
|
"2026-01-01",
|
"2025-01-01",
|
"2024-01-01",
|
"2023-01-01",
|
"2022-01-01",
|
],
|
customerTrends: [
|
{ 内蒙古: 100, 银川: 200, 自提: 300, 其他: 150, 全部: 750 },
|
{ 内蒙古: 80, 银川: 180, 自提: 280, 其他: 130, 全部: 670 },
|
{ 内蒙古: 90, 银川: 190, 自提: 290, 其他: 140, 全部: 710 },
|
{ 内蒙古: 70, 银川: 170, 自提: 270, 其他: 120, 全部: 630 },
|
{ 内蒙古: 110, 银川: 210, 自提: 310, 其他: 160, 全部: 790 },
|
],
|
};
|
updateAmountSalesData();
|
}
|
};
|
|
// 更新销售金额分析表格数据
|
const updateAmountSalesData = () => {
|
const data = [];
|
const { dates, customerTrends } = salesAmountTableData.value;
|
|
// 提取所有销售区域
|
const areaSet = new Set();
|
customerTrends.forEach(item => {
|
Object.keys(item).forEach(key => areaSet.add(key));
|
});
|
|
// 更新销售区域列表,确保"全部"在第一位
|
tableSalesAreas.value = [
|
"全部",
|
...Array.from(areaSet).filter(area => area !== "全部"),
|
];
|
|
// 确保选中的区域在列表中
|
if (!tableSalesAreas.value.includes(tableSelectedArea.value)) {
|
tableSelectedArea.value = "全部";
|
}
|
|
// 生成表格数据
|
dates.forEach((date, index) => {
|
const trends = customerTrends[index] || {};
|
Object.keys(trends).forEach(area => {
|
data.push({
|
period: date,
|
area: area,
|
productType: tableProductType2.value,
|
sales: trends[area],
|
sort: data.length + 1,
|
});
|
});
|
});
|
|
amountSalesData.value = data;
|
};
|
|
// 筛选后的销售金额分析表格数据
|
const filteredAmountSalesData = computed(() => {
|
if (tableSelectedArea.value === "全部") {
|
// 按年月分组汇总数据
|
const groupedData = {};
|
amountSalesData.value.forEach(item => {
|
const key = item.period;
|
if (!groupedData[key]) {
|
groupedData[key] = {
|
period: item.period,
|
area: "全部",
|
productType: tableProductType2.value,
|
sales: 0,
|
};
|
}
|
groupedData[key].sales += item.sales;
|
});
|
// 转换为数组并按年月排序
|
return Object.values(groupedData).sort((a, b) => {
|
return new Date(b.period) - new Date(a.period);
|
});
|
} else {
|
return amountSalesData.value.filter(
|
item => item.area === tableSelectedArea.value
|
);
|
}
|
});
|
|
// 销售金额分析表格总计
|
const filteredAmountSalesTotal = computed(() => {
|
return filteredAmountSalesData.value.reduce(
|
(total, item) => total + item.sales,
|
0
|
);
|
});
|
|
// 处理销售金额分析图表时间维度变化(右上)
|
const handleChartTimeDimensionChange2 = dimension => {
|
chartTimeDimension2.value = dimension;
|
fetchSalesAmountChartData();
|
};
|
|
// 处理销售金额分析图表产品类型变化(右上)
|
const handleChartProductTypeChange2 = type => {
|
chartProductType2.value = type;
|
fetchSalesAmountChartData();
|
};
|
|
// 处理销售额数据统计表格时间维度变化(右下)
|
const handleTableTimeDimensionChange2 = dimension => {
|
tableTimeDimension2.value = dimension;
|
fetchSalesAmountTableData();
|
// 重新启动滚动,根据时间维度决定是否滚动
|
startAmountTableScroll();
|
};
|
|
// 处理销售额数据统计表格产品类型变化(右下)
|
const handleTableProductTypeChange2 = type => {
|
tableProductType2.value = type;
|
fetchSalesAmountTableData();
|
};
|
|
// 处理销售额数据统计表格销售区变化(右下)
|
const handleTableAreaChange2 = area => {
|
tableSelectedArea.value = area;
|
};
|
|
// 启动销售金额分析表格滚动
|
const startAmountTableScroll = () => {
|
if (amountScrollTimer.value) {
|
clearInterval(amountScrollTimer.value);
|
}
|
// 只有当时间维度不是"年"时才启动滚动
|
if (tableTimeDimension2.value === "年") {
|
return;
|
}
|
|
const scrollTable = () => {
|
if (!boardTableBody.value) return;
|
const rows = boardTableBody.value.querySelectorAll("tr");
|
if (rows.length === 0) return;
|
|
const rowHeight = rows[0].offsetHeight;
|
|
boardTableBody.value.style.transition = "transform 0.5s ease-in-out";
|
boardTableBody.value.style.transform = `translateY(-${rowHeight}px)`;
|
|
setTimeout(() => {
|
boardTableBody.value.style.transition = "none";
|
boardTableBody.value.style.transform = "translateY(0)";
|
|
// 直接操作DOM,将第一行移到最后
|
const firstRow = rows[0];
|
boardTableBody.value.removeChild(firstRow);
|
boardTableBody.value.appendChild(firstRow);
|
}, 500);
|
};
|
|
amountScrollTimer.value = setInterval(scrollTable, 2000);
|
};
|
|
// 监听销售金额分析图表时间维度和产品类型变化(右上)
|
watch([chartTimeDimension2, chartProductType2], async () => {
|
// await fetchSalesAmountChartData();
|
});
|
|
// 监听销售额数据统计表格时间维度和产品类型变化(右下)
|
watch([tableTimeDimension2, tableProductType2], async () => {
|
// await fetchSalesAmountTableData();
|
});
|
|
// 获取产品类型标签类型
|
const getProductTypeType = type => {
|
const typeMap = {
|
砌块: "primary",
|
板材: "success",
|
型材: "warning",
|
};
|
return typeMap[type] || "info";
|
};
|
|
// 获取销售区域标签类型
|
const getSalesAreaType = area => {
|
const typeMap = {
|
华东: "primary",
|
华北: "success",
|
华南: "warning",
|
西南: "danger",
|
西北: "info",
|
};
|
return typeMap[area] || "info";
|
};
|
|
// 组件卸载时销毁图表实例
|
onBeforeUnmount(() => {
|
if (timeTicker) {
|
clearInterval(timeTicker);
|
timeTicker = null;
|
}
|
|
if (salesVolumeChartInstance) {
|
salesVolumeChartInstance.dispose();
|
}
|
if (salesAmountChartInstance) {
|
salesAmountChartInstance.dispose();
|
}
|
if (productTypeChartInstance) {
|
productTypeChartInstance.dispose();
|
}
|
if (salesAreaChartInstance) {
|
salesAreaChartInstance.dispose();
|
}
|
|
if (productTypeTrendChartInstance) {
|
productTypeTrendChartInstance.dispose();
|
}
|
if (cumulativeSalesVolumeChartInstance) {
|
cumulativeSalesVolumeChartInstance.dispose();
|
}
|
if (cumulativeSalesAmountChartInstance) {
|
cumulativeSalesAmountChartInstance.dispose();
|
}
|
|
// 移除窗口大小变化监听
|
window.removeEventListener("resize", handleResize);
|
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
});
|
</script>
|
|
<style scoped>
|
.sales-statistics-container {
|
position: relative;
|
width: 100%;
|
min-height: calc(100vh - 8.4vh);
|
overflow: hidden;
|
color: #b8c8e0;
|
background: #041026;
|
}
|
|
.sales-statistics-container.is-fullscreen {
|
min-height: 100vh;
|
height: 100vh;
|
}
|
|
/* 深色背景图 */
|
.bi-bg {
|
position: absolute;
|
inset: 0;
|
/* background-image: url("@/assets/BI/backImage@2x.png"); */
|
background-size: cover;
|
background-position: center;
|
background-repeat: no-repeat;
|
z-index: 0;
|
}
|
|
/* 顶部标题栏 */
|
.bi-topbar {
|
position: relative;
|
z-index: 2;
|
height: 5.8vh;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.bi-topbar-title-bg {
|
position: absolute;
|
top: 0;
|
left: 0;
|
height: 8vh;
|
width: 100%;
|
object-fit: cover;
|
z-index: 0;
|
pointer-events: none;
|
}
|
|
.bi-topbar-content {
|
position: relative;
|
z-index: 1;
|
width: 100%;
|
padding: 0 2.8vh;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.bi-topbar-title {
|
position: absolute;
|
left: 50%;
|
transform: translateX(-50%);
|
font-size: 2.6vh;
|
font-weight: 800;
|
letter-spacing: 0.1vh;
|
background: linear-gradient(180deg, #ffffff 0%, #b8dfff 100%);
|
-webkit-background-clip: text;
|
background-clip: text;
|
-webkit-text-fill-color: transparent;
|
color: transparent;
|
text-shadow: 0 0 2.6vh rgba(0, 164, 237, 0.55);
|
}
|
|
.bi-topbar-left {
|
position: absolute;
|
left: 1vh;
|
display: flex;
|
align-items: center;
|
gap: 0.8vh;
|
color: rgba(208, 231, 255, 0.85);
|
font-size: 1.3vh;
|
}
|
|
.status-sun {
|
color: #ffd85e;
|
text-shadow: 0 0 1vh rgba(255, 216, 94, 0.8);
|
font-size: 1.3vh;
|
line-height: 1;
|
}
|
|
.bi-topbar-meta {
|
position: absolute;
|
right: 5.2vh;
|
/* top: 1.6vh; */
|
font-size: 1.2vh;
|
font-weight: 500;
|
letter-spacing: 0.05vh;
|
color: rgba(208, 231, 255, 0.85);
|
display: flex;
|
align-items: center;
|
gap: 1vh;
|
}
|
|
.fullscreen-btn {
|
position: absolute;
|
bottom: -1vh;
|
transform: none;
|
border: 0.1vh solid rgba(64, 158, 255, 0.45);
|
background: rgba(0, 164, 237, 0.14);
|
color: #d0e7ff;
|
width: 3.4vh;
|
height: 3.4vh;
|
border-radius: 0.6vh;
|
padding: 0;
|
cursor: pointer;
|
transition: all 0.2s ease;
|
z-index: 10;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.fullscreen-btn:hover {
|
background: rgba(0, 164, 237, 0.24);
|
box-shadow: 0 0 1.2vh rgba(0, 164, 237, 0.3);
|
}
|
|
.bi-topbar-sep {
|
opacity: 0.7;
|
}
|
|
/* 主体网格布局 */
|
.bi-dashboard-grid {
|
position: relative;
|
z-index: 2;
|
height: calc(100vh - 8.4vh - 5.8vh);
|
min-height: 45vh;
|
padding: 1vh 1.8vh 1.4vh;
|
display: grid;
|
grid-template-columns: 1fr 1.05fr 1fr;
|
grid-template-rows: 1fr 1fr;
|
gap: 1.2vh;
|
}
|
|
.sales-statistics-container.is-fullscreen .bi-dashboard-grid {
|
height: calc(100vh - 5.8vh);
|
}
|
|
.bi-panel {
|
background: rgba(3, 18, 46, 0.62);
|
border: 0.1vh solid rgba(64, 158, 255, 0.35);
|
border-radius: 0.4vh;
|
overflow: hidden;
|
box-shadow: 0 0 2.2vh rgba(0, 164, 237, 0.12);
|
display: flex;
|
flex-direction: column;
|
position: relative;
|
}
|
|
.bi-panel-title {
|
height: 4.4vh;
|
display: flex;
|
align-items: center;
|
padding: 0 1.8vh;
|
font-size: 1.5vh;
|
font-weight: 700;
|
color: #b8c8e0;
|
background: linear-gradient(
|
90deg,
|
rgba(0, 164, 237, 0.2),
|
rgba(0, 164, 237, 0.04)
|
);
|
border-bottom: 0.1vh solid rgba(64, 158, 255, 0.25);
|
}
|
|
.panel-tabs,
|
.panel-tabs2 {
|
position: absolute;
|
top: 0.8vh;
|
display: flex;
|
gap: 0.6vh;
|
z-index: 4;
|
}
|
|
.panel-tabs {
|
right: 1.2vh;
|
}
|
|
.panel-tabs2 {
|
right: 8vh;
|
}
|
.tab-item {
|
font-size: 1.2vh;
|
color: rgba(184, 200, 224, 0.75);
|
padding: 0.1vh 0.5vh;
|
border: 0.1vh solid rgba(64, 158, 255, 0.25);
|
border-radius: 0.3vh;
|
line-height: 1.4;
|
cursor: pointer;
|
}
|
|
.tab-item.active {
|
color: #ffffff;
|
border-color: rgba(0, 164, 237, 0.65);
|
background: rgba(0, 164, 237, 0.22);
|
}
|
|
.bi-panel-body {
|
flex: 1;
|
padding: 0.8vh 1vh;
|
position: relative;
|
}
|
|
.scroll-table-container {
|
height: 33vh;
|
overflow: hidden;
|
position: relative;
|
}
|
|
.scroll-table {
|
width: 100%;
|
border-collapse: collapse;
|
color: #b8c8e0;
|
}
|
|
.scroll-table th {
|
/* background-color: #0e2a54; */
|
padding: 1.2vh;
|
text-align: left;
|
font-size: 1.2vh;
|
font-weight: bold;
|
border: 1px solid rgba(184, 200, 224, 0.2);
|
}
|
|
.scroll-table td {
|
padding: 1vh;
|
font-size: 1.1vh;
|
border-bottom: 1px solid rgba(184, 200, 224, 0.1);
|
}
|
|
.scroll-table tbody {
|
display: block;
|
height: 35vh;
|
overflow: hidden;
|
}
|
|
.scroll-table thead,
|
.scroll-table tbody tr {
|
display: table;
|
width: 100%;
|
table-layout: fixed;
|
}
|
|
.scroll-table th,
|
.scroll-table td {
|
width: 25%;
|
box-sizing: border-box;
|
font-size: 1.4vh;
|
}
|
|
.scroll-table tbody tr {
|
transition: all 0.5s ease-in-out;
|
}
|
|
/* .scroll-table tbody tr:nth-child(odd) {
|
background-color: rgba(64, 158, 255, 0.05);
|
}
|
|
.scroll-table tbody tr:nth-child(even) {
|
background-color: rgba(64, 158, 255, 0.1);
|
} */
|
.oddTableTr {
|
background-color: rgba(64, 158, 255, 0.05);
|
}
|
.evenTableTr {
|
background-color: rgba(64, 158, 255, 0.1);
|
}
|
|
.scroll-table-container:hover tbody {
|
overflow: hidden;
|
}
|
|
.echart-fill {
|
width: 100%;
|
height: 100%;
|
}
|
|
.chart-filter-tabs {
|
display: flex;
|
gap: 0.6vh;
|
margin: 0 0 0.5vh 0;
|
justify-self: end;
|
}
|
|
.cf-tab {
|
font-size: 1.1vh;
|
color: rgba(184, 200, 224, 0.68);
|
background: rgba(18, 56, 106, 0.65);
|
border: 0.1vh solid rgba(64, 158, 255, 0.25);
|
padding: 0.3vh 0.9vh;
|
line-height: 1;
|
cursor: pointer;
|
}
|
|
.cf-tab.active {
|
color: #d9ecff;
|
background: rgba(0, 108, 208, 0.85);
|
border-color: rgba(64, 158, 255, 0.65);
|
}
|
|
.chart-unit-row {
|
display: flex;
|
justify-content: space-between;
|
font-size: 1.2vh;
|
color: rgba(208, 231, 255, 0.88);
|
margin-bottom: 0.4vh;
|
padding: 0 0.2vh;
|
}
|
|
.dot-legend::before {
|
content: "";
|
display: inline-block;
|
width: 0.8vh;
|
height: 0.8vh;
|
background: #65a0ff;
|
margin-right: 0.6vh;
|
}
|
|
.chart-mini-title {
|
display: flex;
|
align-items: center;
|
gap: 0.8vh;
|
font-size: 1.8vh;
|
color: #d9ecff;
|
}
|
|
/* 面板底部合计行 */
|
.panel-summary-row {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding: 0.8vh 1.2vh;
|
/* margin-top: 0.8vh; */
|
background: #041e3c;
|
border-top: 0.1vh solid rgba(64, 158, 255, 0.25);
|
border-radius: 0 0 0.4vh 0.4vh;
|
width: 100%;
|
position: absolute;
|
bottom: 0;
|
}
|
|
.summary-label {
|
font-size: 1.3vh;
|
font-weight: 700;
|
color: #b8c8e0;
|
}
|
|
.summary-value {
|
font-size: 1.4vh;
|
font-weight: 800;
|
color: #00a4ed;
|
margin-right: 3.8vh;
|
text-shadow: 0 0 1vh rgba(0, 164, 237, 0.5);
|
}
|
|
.summary-value2 {
|
font-size: 1.4vh;
|
font-weight: 800;
|
color: #00a4ed;
|
margin-right: 1.8vh;
|
text-shadow: 0 0 1vh rgba(0, 164, 237, 0.5);
|
}
|
.diamond {
|
width: 1vh;
|
height: 1vh;
|
background: #1e8bff;
|
transform: rotate(45deg);
|
display: inline-block;
|
}
|
|
.chart-unit-single {
|
justify-content: flex-start;
|
margin-bottom: 0.2vh;
|
}
|
|
.bi-panel-top-left .echart-fill,
|
.bi-panel-top-right .echart-fill {
|
height: calc(100% - 4.4vh);
|
}
|
|
.bi-panel-bottom-left .echart-fill,
|
.bi-panel-bottom-right .echart-fill {
|
height: calc(100% - 2.8vh);
|
}
|
|
.bi-panel-bottom-center .echart-fill {
|
height: calc(100% - 4.4vh);
|
}
|
|
.bi-panel-top-left {
|
grid-column: 1;
|
grid-row: 1;
|
position: relative;
|
}
|
|
.bi-panel-top-right {
|
grid-column: 3;
|
grid-row: 1;
|
position: relative;
|
}
|
|
.bi-panel-bottom-left {
|
grid-column: 1;
|
grid-row: 2;
|
overflow-y: auto;
|
}
|
.bi-panel-bottom-left::-webkit-scrollbar {
|
width: 0vh;
|
height: 0vh;
|
}
|
|
.bi-panel-bottom-center {
|
grid-column: 2;
|
grid-row: 2;
|
}
|
|
.bi-panel-bottom-right {
|
grid-column: 3;
|
grid-row: 2;
|
overflow-y: auto;
|
}
|
.bi-panel-bottom-right::-webkit-scrollbar {
|
width: 0vh;
|
height: 0vh;
|
}
|
|
/* 中心环浮层(绝对定位在网格上方) */
|
.center-ring {
|
grid-column: 2;
|
grid-row: 1 / span 2;
|
position: absolute;
|
background: url("@/assets/BI/imageSS@2x.png") no-repeat bottom center;
|
background-size: 100% 30%;
|
left: 25%;
|
top: 25%;
|
transform: translate(-50%, -50%);
|
width: 60vh;
|
height: 40.5vh;
|
z-index: 3;
|
pointer-events: none;
|
}
|
.center-ring-box {
|
position: absolute;
|
/* inset: 0; */
|
height: 100%;
|
width: 100%;
|
/* background-color: #fff; */
|
background: url("@/assets/BI/imageSStop.png") no-repeat center center;
|
background-size: 80% 90%;
|
}
|
|
.center-ring-bg {
|
width: 100%;
|
height: 100%;
|
object-fit: contain;
|
filter: drop-shadow(0 0 2vh rgba(0, 164, 237, 0.35));
|
}
|
|
.center-ring-content {
|
position: absolute;
|
inset: 0;
|
}
|
|
.center-ring-content::before,
|
.center-ring-content::after {
|
content: "";
|
position: absolute;
|
left: 50%;
|
top: 56%;
|
width: 37vh;
|
height: 14.6vh;
|
transform: translate(-50%, -50%) rotate(-18deg);
|
border: 0.2vh solid rgba(40, 186, 255, 0.45);
|
border-radius: 50%;
|
filter: drop-shadow(0 0 0.8vh rgba(0, 164, 237, 0.35));
|
opacity: 0.7;
|
}
|
|
.center-ring-content::after {
|
width: 36vh;
|
height: 15vh;
|
transform: translate(-50%, -50%) rotate(26deg);
|
border-color: rgba(80, 220, 255, 0.35);
|
opacity: 0.55;
|
}
|
|
.center-ring-title {
|
position: absolute;
|
top: 50%;
|
left: 50%;
|
transform: translate(-50%, -50%);
|
font-size: 3.6vh;
|
line-height: 1.05;
|
text-align: center;
|
font-weight: 900;
|
color: #eaf6ff;
|
text-shadow: 0 0 2.2vh rgba(0, 164, 237, 0.55);
|
z-index: 2;
|
}
|
|
.center-ring-title::before {
|
content: "";
|
position: absolute;
|
left: 50%;
|
top: 50%;
|
width: 15.5vh;
|
height: 15.5vh;
|
transform: translate(-50%, -50%);
|
background: radial-gradient(
|
circle,
|
rgba(43, 199, 255, 0.26) 0%,
|
rgba(8, 28, 61, 0.86) 70%
|
);
|
border: 0.2vh solid rgba(39, 198, 255, 0.46);
|
border-radius: 50%;
|
box-shadow: 0 0 2vh rgba(0, 164, 237, 0.45),
|
inset 0 0 2.6vh rgba(0, 164, 237, 0.2);
|
z-index: -1;
|
}
|
|
.center-metric {
|
position: absolute;
|
width: 15.5vh;
|
z-index: 3;
|
text-align: center;
|
height: 12vh;
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
align-items: center;
|
}
|
|
.center-metric-label {
|
font-size: 1.2vh;
|
font-weight: 500;
|
color: rgba(234, 246, 255, 0.9);
|
margin-top: 0;
|
}
|
|
.center-metric-value {
|
font-size: 3.4vh;
|
font-weight: 800;
|
color: #eaf6ff;
|
text-shadow: 0 0 0.8vh rgba(0, 229, 255, 0.22);
|
line-height: 1;
|
}
|
|
.center-metric-unit {
|
margin-top: 0;
|
font-size: 1.2vh;
|
color: rgba(208, 231, 255, 0.85);
|
}
|
|
.m1 {
|
top: 2.5vh;
|
left: 4.8vh;
|
text-align: left;
|
}
|
|
.m2 {
|
top: 4.1vh;
|
right: 8.6vh;
|
text-align: right;
|
}
|
|
.m3 {
|
bottom: 7.9vh;
|
left: 4vh;
|
text-align: left;
|
}
|
|
.m4 {
|
bottom: 7vh;
|
right: 5.4vh;
|
text-align: right;
|
}
|
|
@media (max-width: 1100px) {
|
.bi-topbar-content {
|
padding: 0 1.4vh;
|
}
|
.center-ring {
|
left: 45.2%;
|
width: 33vh;
|
height: 24.5vh;
|
top: 2.4vh;
|
}
|
.center-ring-title {
|
top: 50%;
|
font-size: 2.8vh;
|
transform: translate(-50%, -50%);
|
}
|
.center-metric {
|
height: 10.5vh;
|
}
|
.m1 {
|
top: 5.2vh;
|
left: 4.2vh;
|
}
|
.m2 {
|
top: 5.4vh;
|
right: 4.2vh;
|
}
|
.m3 {
|
bottom: 6.2vh;
|
left: 4.8vh;
|
}
|
.m4 {
|
bottom: 6.8vh;
|
right: 4.4vh;
|
}
|
}
|
.scroll-table-content {
|
height: 39vh;
|
overflow: auto;
|
}
|
.scroll-table-content::-webkit-scrollbar {
|
width: 0;
|
height: 0;
|
background-color: transparent;
|
}
|
.total-row {
|
position: absolute;
|
bottom: 0vh;
|
left: 0;
|
width: 100%;
|
display: flex;
|
height: 3.5vh;
|
justify-content: space-around;
|
align-items: center;
|
background-color: #081843;
|
}
|
|
.total-cell {
|
width: 20%;
|
font-size: 1.4vh;
|
margin-bottom: 0.5vh;
|
line-height: 3.5vh;
|
padding-left: 0.8vh;
|
color: #eaf6ff;
|
text-shadow: 0 0 0.8vh rgba(0, 229, 255, 0.22);
|
text-align: left;
|
color: #c3c3c3;
|
}
|
.total-cell2 {
|
width: 20%;
|
font-size: 1.4vh;
|
margin-bottom: 0.5vh;
|
line-height: 3.5vh;
|
color: #eaf6ff;
|
text-shadow: 0 0 0.8vh rgba(0, 229, 255, 0.22);
|
text-align: left;
|
color: #c3c3c3;
|
}
|
</style>
|