<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">
|
<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>
|
<button class="fullscreen-btn" @click="toggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏显示'">
|
<svg v-if="!isFullscreen" width="16" height="16" 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="16" height="16" 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>
|
</div>
|
</div>
|
|
<div class="bi-dashboard-grid">
|
<!-- 左上:销量趋势 -->
|
<div class="bi-panel bi-panel-top-left">
|
<PanelHeader title="销售分析-砌块" />
|
<div class="panel-tabs">
|
<span class="tab-item active">年</span>
|
<span class="tab-item">月</span>
|
</div>
|
<div class="bi-panel-body">
|
<div class="chart-filter-tabs">
|
<span class="cf-tab active">***销售区</span>
|
<span class="cf-tab">xxx销售区</span>
|
<span class="cf-tab">xxx销售区</span>
|
<span class="cf-tab">xxx销售区</span>
|
<span class="cf-tab">xxx销售区</span>
|
</div>
|
<div class="chart-unit-row">
|
<span>单位:立方米</span>
|
<span class="dot-legend">板材</span>
|
</div>
|
<div ref="salesVolumeChart" class="echart-fill"></div>
|
</div>
|
</div>
|
|
<!-- 右上:销售金额 -->
|
<div class="bi-panel bi-panel-top-right">
|
<PanelHeader title="销售分析-板材" />
|
<div class="panel-tabs">
|
<span class="tab-item active">年</span>
|
<span class="tab-item">月</span>
|
</div>
|
<div class="bi-panel-body">
|
<div class="chart-filter-tabs">
|
<span class="cf-tab active">***销售区</span>
|
<span class="cf-tab">xxx销售区</span>
|
<span class="cf-tab">xxx销售区</span>
|
<span class="cf-tab">xxx销售区</span>
|
<span class="cf-tab">xxx销售区</span>
|
</div>
|
<div class="chart-unit-row">
|
<span>单位:件</span>
|
<span class="dot-legend">板材</span>
|
</div>
|
<div ref="salesAmountChart" class="echart-fill"></div>
|
</div>
|
</div>
|
|
<!-- 中间中心环 -->
|
<div class="center-ring">
|
<img
|
class="center-ring-bg"
|
src="@/assets/BI/zonghetongbingtubiankuang@2x.png"
|
alt=""
|
/>
|
<div class="center-ring-content">
|
<div class="center-ring-title">销售<br />中心</div>
|
|
<div class="center-metric m1">
|
<div class="center-metric-label">新增客户</div>
|
<div class="center-metric-value">{{ centerNewCustomerCount }}</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">{{ salesOrderCount }}</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 }}</div>
|
<div class="center-metric-unit">区</div>
|
</div>
|
</div>
|
</div>
|
|
<!-- 左下:产品类型销量 -->
|
<div class="bi-panel bi-panel-bottom-left">
|
<PanelHeader title="客户销量排名分析-砌块" />
|
<div class="panel-tabs">
|
<span class="tab-item active">年</span>
|
<span class="tab-item">月</span>
|
</div>
|
<div class="bi-panel-body">
|
<div class="chart-filter-tabs">
|
<span class="cf-tab active">***销售区</span>
|
<span class="cf-tab">xxx销售区</span>
|
<span class="cf-tab">xxx销售区</span>
|
<span class="cf-tab">xxx销售区</span>
|
<span class="cf-tab">xxx销售区</span>
|
</div>
|
<div ref="productTypeChart" class="echart-fill"></div>
|
</div>
|
</div>
|
|
<!-- 中下:新增客户分析(分产品类型趋势) -->
|
<div class="bi-panel bi-panel-bottom-center">
|
<PanelHeader title="新增客户趋势分析" />
|
<div class="panel-tabs">
|
<span class="tab-item active">年</span>
|
<span class="tab-item">月</span>
|
</div>
|
<div class="bi-panel-body">
|
<div class="chart-mini-title">
|
<span class="diamond"></span>
|
<span>新增客户数</span>
|
</div>
|
<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 title="客户销量排名分析-板材" />
|
<div class="panel-tabs">
|
<span class="tab-item active">年</span>
|
<span class="tab-item">月</span>
|
</div>
|
<div class="bi-panel-body">
|
<div class="chart-filter-tabs">
|
<span class="cf-tab active">***销售区</span>
|
<span class="cf-tab">xxx销售区</span>
|
<span class="cf-tab">xxx销售区</span>
|
<span class="cf-tab">xxx销售区</span>
|
<span class="cf-tab">xxx销售区</span>
|
</div>
|
<div ref="salesAreaChart" class="echart-fill"></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";
|
|
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 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 = computed(() => {
|
return filteredData.value
|
.reduce((sum, item) => sum + item.salesAmount, 0)
|
.toFixed(2);
|
});
|
|
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 centerNewCustomerCount = computed(() => 112);
|
const completedOrders = computed(() => 1829);
|
const salesOrderCount = computed(() => 34);
|
const totalSalesAreaCount = computed(() => 12);
|
|
// 变化率计算(模拟)
|
const salesVolumeChange = ref("+5.2");
|
const salesAmountChange = ref("+7.8");
|
const customerCountChange = ref("+3.5");
|
const totalCustomerChange = ref("+2.1");
|
|
// 表格数据
|
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 salesVolumeChartOption = computed(() => {
|
const periods = ["6/9", "6/10", "6/11", "6/12", "6/12", "6/13"];
|
const values = [132, 168, 168, 198, 168, 198];
|
|
return {
|
backgroundColor: "transparent",
|
tooltip: {
|
trigger: "axis",
|
backgroundColor: "rgba(0,0,0,0.55)",
|
borderColor: "rgba(64,158,255,0.25)",
|
borderWidth: 1,
|
textStyle: { color: "#B8C8E0" },
|
formatter: "{b}: {c} 立方米",
|
},
|
grid: {
|
left: "10%",
|
right: "4%",
|
bottom: "16%",
|
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: 11, margin: 10 },
|
splitLine: { show: false },
|
},
|
yAxis: {
|
type: "value",
|
name: "",
|
axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } },
|
axisLabel: { color: "#B8C8E0", fontSize: 11, margin: 8 },
|
splitLine: { lineStyle: { color: "rgba(184,200,224,0.12)" } },
|
},
|
series: [
|
{
|
data: values,
|
type: "line",
|
smooth: true,
|
symbolSize: 8,
|
lineStyle: { width: 3, color: "#00A4ED" },
|
itemStyle: { color: "#00A4ED" },
|
areaStyle: {
|
opacity: 1,
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: "rgba(0,164,237,0.35)" },
|
{ offset: 1, color: "rgba(0,164,237,0)" },
|
]),
|
},
|
},
|
],
|
};
|
});
|
|
// 销售金额趋势图表配置
|
const salesAmountChartOption = computed(() => {
|
const periods = ["6/9", "6/10", "6/11", "6/12", "6/12", "6/13"];
|
const values = [132, 168, 168, 198, 168, 198];
|
|
return {
|
backgroundColor: "transparent",
|
tooltip: {
|
trigger: "axis",
|
backgroundColor: "rgba(0,0,0,0.55)",
|
borderColor: "rgba(64,158,255,0.25)",
|
borderWidth: 1,
|
textStyle: { color: "#B8C8E0" },
|
formatter: "{b}: {c} 万元",
|
},
|
grid: {
|
left: "10%",
|
right: "4%",
|
bottom: "16%",
|
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: 11, margin: 10 },
|
},
|
yAxis: {
|
type: "value",
|
name: "",
|
axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } },
|
axisLabel: { color: "#B8C8E0", fontSize: 11, margin: 8 },
|
splitLine: { lineStyle: { color: "rgba(184,200,224,0.12)" } },
|
},
|
series: [
|
{
|
data: values,
|
type: "line",
|
smooth: true,
|
symbolSize: 8,
|
itemStyle: {
|
color: "#00A4ED",
|
},
|
lineStyle: { width: 3, color: "#00A4ED" },
|
areaStyle: {
|
opacity: 1,
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: "rgba(0,164,237,0.35)" },
|
{ offset: 1, color: "rgba(0,164,237,0)" },
|
]),
|
},
|
},
|
],
|
};
|
});
|
|
// 产品类型销量图表配置(横向柱状)
|
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: 1,
|
textStyle: { color: "#B8C8E0" },
|
formatter: "{b}: {c} 立方米",
|
},
|
grid: {
|
left: "14%",
|
right: "6%",
|
top: "16%",
|
bottom: "8%",
|
containLabel: true,
|
},
|
xAxis: {
|
type: "value",
|
axisLine: { show: false },
|
axisLabel: { color: "#B8C8E0", fontSize: 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: 11, margin: 8 },
|
},
|
series: [
|
{
|
name: "销量(立方米)",
|
type: "bar",
|
barWidth: 14,
|
data: values,
|
itemStyle: {
|
color: params => barColors[params.dataIndex] || "#00A4ED",
|
borderRadius: [6, 6, 6, 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: 1,
|
textStyle: { color: "#B8C8E0" },
|
formatter: "{b}: {c} 立方米",
|
},
|
grid: {
|
left: "14%",
|
right: "6%",
|
top: "16%",
|
bottom: "8%",
|
containLabel: true,
|
},
|
xAxis: {
|
type: "value",
|
axisLine: { show: false },
|
axisLabel: { color: "#B8C8E0", fontSize: 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: 11, margin: 8 },
|
},
|
series: [
|
{
|
name: "销量(立方米)",
|
type: "bar",
|
barWidth: 14,
|
data: values,
|
itemStyle: {
|
color: params => barColors[params.dataIndex] || "#00A4ED",
|
borderRadius: [6, 6, 6, 6],
|
},
|
},
|
],
|
};
|
});
|
|
// 新增客户趋势图表配置(按产品类型多折线)
|
const productTypeTrendChartOption = computed(() => {
|
const typeOrder = ["AAA销售区", "BBB销售区", "CCC销售区", "DDD销售区"];
|
const colorMap = {
|
"AAA销售区": "#65A0FF",
|
"BBB销售区": "#33F5FF",
|
"CCC销售区": "#FFD54A",
|
"DDD销售区": "#EE52FF",
|
};
|
const areaColorMap = {
|
"AAA销售区": "rgba(101,160,255,0.28)",
|
"BBB销售区": "rgba(51,245,255,0.30)",
|
"CCC销售区": "rgba(255,213,74,0.25)",
|
"DDD销售区": "rgba(238,82,255,0.25)",
|
};
|
const periods = ["6/9", "6/10", "6/11", "6/12", "6/12", "6/13", "6/14", "6/15"];
|
const map = {
|
"AAA销售区": [85, 112, 112, 112, 140, 112, 112, 140],
|
"BBB销售区": [140, 180, 180, 180, 230, 180, 180, 230],
|
"CCC销售区": [112, 140, 140, 140, 180, 140, 140, 180],
|
"DDD销售区": [200, 165, 200, 200, 165, 165, 140, 140],
|
};
|
|
const series = typeOrder.map((t) => ({
|
name: t,
|
type: "line",
|
smooth: true,
|
symbolSize: 7,
|
showSymbol: true,
|
data: map[t] || [],
|
lineStyle: { width: 3, color: colorMap[t] },
|
itemStyle: { color: colorMap[t] },
|
areaStyle: {
|
opacity: 0.25,
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: areaColorMap[t] },
|
{ offset: 1, color: "rgba(0,0,0,0)" },
|
]),
|
},
|
}));
|
|
return {
|
backgroundColor: "transparent",
|
legend: {
|
top: 10,
|
left: "center",
|
textStyle: { color: "#B8C8E0", fontSize: 11, padding: [0, 0, 0, 2] },
|
itemWidth: 12,
|
itemHeight: 10,
|
itemGap: 18,
|
},
|
tooltip: {
|
trigger: "axis",
|
backgroundColor: "rgba(0,0,0,0.55)",
|
borderColor: "rgba(64,158,255,0.25)",
|
borderWidth: 1,
|
textStyle: { color: "#B8C8E0" },
|
},
|
grid: {
|
left: "10%",
|
right: "6%",
|
bottom: "14%",
|
top: "26%",
|
containLabel: true,
|
},
|
xAxis: {
|
type: "category",
|
data: periods,
|
axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } },
|
axisTick: { show: false },
|
axisLabel: { color: "#B8C8E0", fontSize: 11, margin: 10 },
|
},
|
yAxis: {
|
type: "value",
|
name: "",
|
axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } },
|
axisLabel: { color: "#B8C8E0", fontSize: 11, margin: 8 },
|
splitLine: { lineStyle: { color: "rgba(184,200,224,0.12)" } },
|
},
|
series,
|
};
|
});
|
|
// 累计销量趋势图表配置
|
const cumulativeSalesVolumeChartOption = computed(() => {
|
// 按周期分组
|
const periodMap = {};
|
let cumulativeValue = 0;
|
|
// 按周期排序
|
const sortedData = [...filteredData.value].sort((a, b) =>
|
a.period.localeCompare(b.period)
|
);
|
|
sortedData.forEach(item => {
|
cumulativeValue += item.salesVolume;
|
periodMap[item.period] = cumulativeValue;
|
});
|
|
const periods = Object.keys(periodMap).sort();
|
const values = periods.map(period => periodMap[period]);
|
|
return {
|
tooltip: {
|
trigger: "axis",
|
formatter: "{b}: {c} 立方米",
|
},
|
xAxis: {
|
type: "category",
|
data: periods,
|
},
|
yAxis: {
|
type: "value",
|
name: "累计销量(立方米)",
|
},
|
series: [
|
{
|
data: values,
|
type: "line",
|
smooth: true,
|
areaStyle: {
|
opacity: 0.3,
|
},
|
itemStyle: {
|
color: "#E6A23C",
|
},
|
lineStyle: {
|
width: 3,
|
},
|
},
|
],
|
};
|
});
|
|
// 累计销售金额趋势图表配置
|
const cumulativeSalesAmountChartOption = computed(() => {
|
// 按周期分组
|
const periodMap = {};
|
let cumulativeValue = 0;
|
|
// 按周期排序
|
const sortedData = [...filteredData.value].sort((a, b) =>
|
a.period.localeCompare(b.period)
|
);
|
|
sortedData.forEach(item => {
|
cumulativeValue += item.salesAmount;
|
periodMap[item.period] = cumulativeValue;
|
});
|
|
const periods = Object.keys(periodMap).sort();
|
const values = periods.map(period => periodMap[period]);
|
|
return {
|
tooltip: {
|
trigger: "axis",
|
formatter: "{b}: {c} 万元",
|
},
|
xAxis: {
|
type: "category",
|
data: periods,
|
},
|
yAxis: {
|
type: "value",
|
name: "累计销售金额(万元)",
|
},
|
series: [
|
{
|
data: values,
|
type: "bar",
|
itemStyle: {
|
color: "#F56C6C",
|
},
|
},
|
],
|
};
|
});
|
|
// 方法
|
const goBack = () => {
|
router.back();
|
};
|
|
const handleDateChange = () => {
|
// 处理日期变化
|
updateCharts();
|
};
|
|
const handleFilterChange = () => {
|
// 处理筛选条件变化
|
updateCharts();
|
};
|
|
// 初始化图表
|
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(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
|
);
|
}
|
};
|
|
// 监听窗口大小变化
|
const handleResize = () => {
|
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(() => {
|
// 启动顶部栏时间刷新
|
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"),
|
];
|
|
// 等待DOM更新后初始化图表
|
nextTick(() => {
|
initCharts();
|
});
|
|
// 添加窗口大小变化监听
|
window.addEventListener("resize", handleResize);
|
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
});
|
|
// 获取产品类型标签类型
|
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 - 84px);
|
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: 58px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.bi-topbar-title-bg {
|
position: absolute;
|
top: 0;
|
left: 0;
|
height: 58px;
|
width: 100%;
|
object-fit: cover;
|
z-index: 0;
|
pointer-events: none;
|
}
|
|
.bi-topbar-content {
|
position: relative;
|
z-index: 1;
|
width: 100%;
|
padding: 0 28px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.bi-topbar-title {
|
position: absolute;
|
left: 50%;
|
transform: translateX(-50%);
|
font-size: 26px;
|
font-weight: 800;
|
letter-spacing: 1px;
|
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 26px rgba(0, 164, 237, 0.55);
|
}
|
|
.bi-topbar-left {
|
position: absolute;
|
left: 10px;
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
color: rgba(208, 231, 255, 0.85);
|
font-size: 13px;
|
}
|
|
.status-sun {
|
color: #ffd85e;
|
text-shadow: 0 0 10px rgba(255, 216, 94, 0.8);
|
font-size: 13px;
|
line-height: 1;
|
}
|
|
.bi-topbar-meta {
|
position: absolute;
|
right: 52px;
|
top: 16px;
|
font-size: 12px;
|
font-weight: 500;
|
letter-spacing: 0.5px;
|
color: rgba(208, 231, 255, 0.85);
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
}
|
|
.fullscreen-btn {
|
position: absolute;
|
right: 10px;
|
top: 12px;
|
transform: none;
|
border: 1px solid rgba(64, 158, 255, 0.45);
|
background: rgba(0, 164, 237, 0.14);
|
color: #d0e7ff;
|
width: 34px;
|
height: 34px;
|
border-radius: 6px;
|
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 12px rgba(0, 164, 237, 0.3);
|
}
|
|
.bi-topbar-sep {
|
opacity: 0.7;
|
}
|
|
/* 主体网格布局 */
|
.bi-dashboard-grid {
|
position: relative;
|
z-index: 2;
|
height: calc(100vh - 84px - 58px);
|
min-height: 450px;
|
padding: 10px 18px 14px;
|
display: grid;
|
grid-template-columns: 1fr 1.05fr 1fr;
|
grid-template-rows: 1fr 1fr;
|
gap: 12px;
|
}
|
|
.sales-statistics-container.is-fullscreen .bi-dashboard-grid {
|
height: calc(100vh - 58px);
|
}
|
|
.bi-panel {
|
background: rgba(3, 18, 46, 0.62);
|
border: 1px solid rgba(64, 158, 255, 0.35);
|
border-radius: 4px;
|
overflow: hidden;
|
box-shadow: 0 0 22px rgba(0, 164, 237, 0.12);
|
display: flex;
|
flex-direction: column;
|
position: relative;
|
}
|
|
.bi-panel-title {
|
height: 44px;
|
display: flex;
|
align-items: center;
|
padding: 0 18px;
|
font-size: 15px;
|
font-weight: 700;
|
color: #B8C8E0;
|
background: linear-gradient(
|
90deg,
|
rgba(0, 164, 237, 0.2),
|
rgba(0, 164, 237, 0.04)
|
);
|
border-bottom: 1px solid rgba(64, 158, 255, 0.25);
|
}
|
|
.panel-tabs {
|
position: absolute;
|
top: 8px;
|
right: 12px;
|
display: flex;
|
gap: 6px;
|
z-index: 4;
|
}
|
|
.tab-item {
|
font-size: 12px;
|
color: rgba(184, 200, 224, 0.75);
|
padding: 1px 5px;
|
border: 1px solid rgba(64, 158, 255, 0.25);
|
border-radius: 3px;
|
line-height: 1.4;
|
}
|
|
.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: 8px 10px;
|
}
|
|
.echart-fill {
|
width: 100%;
|
height: 100%;
|
}
|
|
.chart-filter-tabs {
|
display: flex;
|
gap: 6px;
|
margin: 0 0 5px 0;
|
}
|
|
.cf-tab {
|
font-size: 11px;
|
color: rgba(184, 200, 224, 0.68);
|
background: rgba(18, 56, 106, 0.65);
|
border: 1px solid rgba(64, 158, 255, 0.25);
|
padding: 3px 9px;
|
line-height: 1;
|
}
|
|
.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: 12px;
|
color: rgba(208, 231, 255, 0.88);
|
margin-bottom: 4px;
|
padding: 0 2px;
|
}
|
|
.dot-legend::before {
|
content: "";
|
display: inline-block;
|
width: 8px;
|
height: 8px;
|
background: #65A0FF;
|
margin-right: 6px;
|
}
|
|
.chart-mini-title {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
font-size: 18px;
|
color: #D9ECFF;
|
font-weight: 700;
|
margin: 0 0 8px 0;
|
line-height: 1;
|
}
|
|
.diamond {
|
width: 10px;
|
height: 10px;
|
background: #1E8BFF;
|
transform: rotate(45deg);
|
display: inline-block;
|
}
|
|
.chart-unit-single {
|
justify-content: flex-start;
|
margin-bottom: 2px;
|
}
|
|
.bi-panel-top-left .echart-fill,
|
.bi-panel-top-right .echart-fill {
|
height: calc(100% - 44px);
|
}
|
|
.bi-panel-bottom-left .echart-fill,
|
.bi-panel-bottom-right .echart-fill {
|
height: calc(100% - 28px);
|
}
|
|
.bi-panel-bottom-center .echart-fill {
|
height: calc(100% - 44px);
|
}
|
|
.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;
|
}
|
|
.bi-panel-bottom-center {
|
grid-column: 2;
|
grid-row: 2;
|
}
|
|
.bi-panel-bottom-right {
|
grid-column: 3;
|
grid-row: 2;
|
}
|
|
/* 中心环浮层(绝对定位在网格上方) */
|
.center-ring {
|
grid-column: 2;
|
grid-row: 1 / span 2;
|
position: absolute;
|
left:25%;
|
top: 25%;
|
transform: translate(-50%, -50%);
|
width: 400px;
|
height: 275px;
|
z-index: 3;
|
pointer-events: none;
|
}
|
|
.center-ring-bg {
|
width: 100%;
|
height: 100%;
|
object-fit: contain;
|
filter: drop-shadow(0 0 20px 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: 370px;
|
height: 146px;
|
transform: translate(-50%, -50%) rotate(-18deg);
|
border: 2px solid rgba(40, 186, 255, 0.45);
|
border-radius: 50%;
|
filter: drop-shadow(0 0 8px rgba(0, 164, 237, 0.35));
|
opacity: 0.7;
|
}
|
|
.center-ring-content::after {
|
width: 360px;
|
height: 150px;
|
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: 36px;
|
line-height: 1.05;
|
text-align: center;
|
font-weight: 900;
|
color: #EAF6FF;
|
text-shadow: 0 0 22px rgba(0, 164, 237, 0.55);
|
z-index: 2;
|
}
|
|
.center-ring-title::before {
|
content: "";
|
position: absolute;
|
left: 50%;
|
top: 50%;
|
width: 155px;
|
height: 155px;
|
transform: translate(-50%, -50%);
|
background: radial-gradient(circle, rgba(43, 199, 255, 0.26) 0%, rgba(8, 28, 61, 0.86) 70%);
|
border: 2px solid rgba(39, 198, 255, 0.46);
|
border-radius: 50%;
|
box-shadow: 0 0 20px rgba(0, 164, 237, 0.45), inset 0 0 26px rgba(0, 164, 237, 0.2);
|
z-index: -1;
|
}
|
|
.center-metric {
|
position: absolute;
|
width: 155px;
|
z-index: 3;
|
text-align: center;
|
height: 120px;
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
align-items: center;
|
}
|
|
.center-metric::before {
|
content: "";
|
position: absolute;
|
left: 50%;
|
top: 50%;
|
width: 104px;
|
height: 104px;
|
transform: translate(-50%, -50%);
|
border-radius: 50%;
|
border: 2px solid rgba(71, 223, 255, 0.4);
|
background: radial-gradient(circle, rgba(37, 177, 255, 0.25) 0%, rgba(8, 33, 69, 0.55) 70%);
|
box-shadow: 0 0 18px rgba(0, 164, 237, 0.35), inset 0 0 18px rgba(0, 164, 237, 0.2);
|
z-index: -1;
|
}
|
|
.center-metric-label {
|
font-size: 12px;
|
font-weight: 500;
|
color: rgba(234, 246, 255, 0.9);
|
margin-top: 0;
|
}
|
|
.center-metric-value {
|
font-size: 34px;
|
font-weight: 800;
|
color: #EAF6FF;
|
text-shadow: 0 0 8px rgba(0, 229, 255, 0.22);
|
line-height: 1.0;
|
}
|
|
.center-metric-unit {
|
margin-top: 0;
|
font-size: 12px;
|
color: rgba(208, 231, 255, 0.85);
|
}
|
|
.m1 {
|
top: -6px;
|
left: -26px;
|
text-align: left;
|
}
|
|
.m2 {
|
top: -6px;
|
right: -26px;
|
text-align: right;
|
}
|
|
.m3 {
|
bottom: 66px;
|
left: -30px;
|
text-align: left;
|
}
|
|
.m4 {
|
bottom: 66px;
|
right: -30px;
|
text-align: right;
|
}
|
|
@media (max-width: 1100px) {
|
.bi-topbar-content {
|
padding: 0 14px;
|
}
|
.center-ring {
|
left: 45.2%;
|
width: 330px;
|
height: 245px;
|
top: 24px;
|
}
|
.center-ring-title {
|
top: 50%;
|
font-size: 28px;
|
transform: translate(-50%, -50%);
|
}
|
.center-metric {
|
height: 105px;
|
}
|
.m1 {
|
top: 52px;
|
left: 42px;
|
}
|
.m2 {
|
top: 54px;
|
right: 42px;
|
}
|
.m3 {
|
bottom: 62px;
|
left: 48px;
|
}
|
.m4 {
|
bottom: 68px;
|
right: 44px;
|
}
|
}
|
</style>
|