| | |
| | | <template> |
| | | <div class="sales-statistics-container"> |
| | | <div class="data-dashboard"> |
| | | <!-- 页面标题 --> |
| | | <!-- <div class="dashboard-header"> |
| | | <div class="factory-name">销售统计看板</div> |
| | | </div> --> |
| | | <!-- 筛选条件 --> |
| | | <div class="filter-area"> |
| | | <div class="filter-section"> |
| | | <span class="filter-label">时间范围:</span> |
| | | <el-date-picker v-model="dateRange" |
| | | type="daterange" |
| | | range-separator="至" |
| | | start-placeholder="开始日期" |
| | | end-placeholder="结束日期" |
| | | value-format="YYYY-MM-DD" |
| | | @change="handleDateChange" |
| | | style="width: 240px;" /> |
| | | <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="filter-section"> |
| | | <span class="filter-label">产品类型:</span> |
| | | <el-select v-model="productType" |
| | | placeholder="请选择产品类型" |
| | | @change="handleFilterChange" |
| | | style="width: 160px;"> |
| | | <el-option label="全部" |
| | | value="" /> |
| | | <el-option label="砌块" |
| | | value="block" /> |
| | | <el-option label="板材" |
| | | value="board" /> |
| | | <el-option label="型材" |
| | | value="profile" /> |
| | | </el-select> |
| | | </div> |
| | | <div class="filter-section"> |
| | | <span class="filter-label">销售区域:</span> |
| | | <el-select v-model="salesArea" |
| | | placeholder="请选择销售区域" |
| | | @change="handleFilterChange" |
| | | style="width: 160px;"> |
| | | <el-option label="全部" |
| | | value="" /> |
| | | <el-option label="华东" |
| | | value="east" /> |
| | | <el-option label="华北" |
| | | value="north" /> |
| | | <el-option label="华南" |
| | | value="south" /> |
| | | <el-option label="西南" |
| | | value="southwest" /> |
| | | <el-option label="西北" |
| | | value="northwest" /> |
| | | </el-select> |
| | | </div> |
| | | <div class="filter-section"> |
| | | <span class="filter-label">统计维度:</span> |
| | | <el-select v-model="statDimension" |
| | | placeholder="请选择统计维度" |
| | | @change="handleFilterChange" |
| | | style="width: 160px;"> |
| | | <el-option label="月度" |
| | | value="month" /> |
| | | <el-option label="年度" |
| | | value="year" /> |
| | | </el-select> |
| | | <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 class="dashboard-content"> |
| | | <!-- 核心指标卡片 --> |
| | | <div class="row row-1"> |
| | | <div class="panel-card card-1"> |
| | | <div class="panel-title">合计销量</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value sales-volume-color">{{ totalSalesVolume }}</div> |
| | | <div class="stat-unit">立方米</div> |
| | | <div class="stat-change">{{ salesVolumeChange }}%</div> |
| | | </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: blockTimeDimension === 'year' }" |
| | | @click="handleBlockTimeDimensionChange('year')">年</span> |
| | | <span class="tab-item" |
| | | :class="{ active: blockTimeDimension === 'month' }" |
| | | @click="handleBlockTimeDimensionChange('month')">月</span> |
| | | </div> |
| | | <div class="panel-tabs2"> |
| | | <span class="tab-item" |
| | | :class="{ active: blockProductType === '砌块' }" |
| | | @click="handleBlockProductTypeChange('砌块')">砌块</span> |
| | | <span class="tab-item" |
| | | :class="{ active: blockProductType === '板材' }" |
| | | @click="handleBlockProductTypeChange('板材')">板材</span> |
| | | </div> |
| | | <div class="bi-panel-body"> |
| | | <div class="chart-unit-row"> |
| | | <span>单位:立方米</span> |
| | | </div> |
| | | <div class="panel-card card-2"> |
| | | <div class="panel-title">销售金额</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value sales-amount-color">{{ totalSalesAmount }}</div> |
| | | <div class="stat-unit">万元</div> |
| | | <div class="stat-change">{{ salesAmountChange }}%</div> |
| | | </div> |
| | | </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: boardTimeDimension === 'year' }" |
| | | @click="handleBoardTimeDimensionChange('year')">年</span> |
| | | <span class="tab-item" |
| | | :class="{ active: boardTimeDimension === 'month' }" |
| | | @click="handleBoardTimeDimensionChange('month')">月</span> |
| | | </div> |
| | | <div class="panel-tabs2"> |
| | | <span class="tab-item" |
| | | :class="{ active: boardProductType === '砌块' }" |
| | | @click="handleBoardProductTypeChange('砌块')">砌块</span> |
| | | <span class="tab-item" |
| | | :class="{ active: boardProductType === '板材' }" |
| | | @click="handleBoardProductTypeChange('板材')">板材</span> |
| | | </div> |
| | | <div class="bi-panel-body"> |
| | | <div class="chart-unit-row"> |
| | | <span>单位:元</span> |
| | | </div> |
| | | <div class="panel-card card-3"> |
| | | <div class="panel-title">新增客户</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value new-customer-color">{{ newCustomerCount }}</div> |
| | | <div class="stat-unit">个</div> |
| | | <div class="stat-change">{{ customerCountChange }}%</div> |
| | | </div> |
| | | </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="panel-card card-4"> |
| | | <div class="panel-title">合计客户</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value total-customer-color">{{ totalCustomerCount }}</div> |
| | | <div class="stat-unit">个</div> |
| | | <div class="stat-change">{{ totalCustomerChange }}%</div> |
| | | </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 }}</div> |
| | | <div class="center-metric-unit">区</div> |
| | | </div> |
| | | </div> |
| | | <!-- 销量和销售金额趋势 --> |
| | | <div class="row row-2"> |
| | | <div class="panel-card card-5"> |
| | | <div class="panel-title">销量趋势</div> |
| | | <div class="chart-container"> |
| | | <div ref="salesVolumeChart" |
| | | style="width: 100%; height: 100%;"></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: blockTimeDimension === 'year' }" |
| | | @click="handleBlockTimeDimensionChange('year')">年</span> |
| | | <span class="tab-item" |
| | | :class="{ active: blockTimeDimension === 'month' }" |
| | | @click="handleBlockTimeDimensionChange('month')">月</span> |
| | | </div> |
| | | <div class="panel-tabs2"> |
| | | <span class="tab-item" |
| | | :class="{ active: blockProductType === '砌块' }" |
| | | @click="handleBlockProductTypeChange('砌块')">砌块</span> |
| | | <span class="tab-item" |
| | | :class="{ active: blockProductType === '板材' }" |
| | | @click="handleBlockProductTypeChange('板材')">板材</span> |
| | | </div> |
| | | <div class="bi-panel-body"> |
| | | <div class="chart-filter-tabs"> |
| | | <span v-for="area in salesAreas" |
| | | :key="area" |
| | | class="cf-tab" |
| | | :class="{ active: blockSelectedArea === area }" |
| | | @click="handleBlockAreaChange(area)">{{ area }}</span> |
| | | </div> |
| | | <div class="panel-card card-6"> |
| | | <div class="panel-title">销售金额趋势</div> |
| | | <div class="chart-container"> |
| | | <div ref="salesAmountChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </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="item.sort % 2 === 0 ? 'evenTableTr' : 'oddTableTr'" |
| | | v-for="(item, index) in blockSalesData" |
| | | :key="item.period + item.area + index"> |
| | | <td>{{ item.sort }}</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">127384 m³</div> |
| | | </div> |
| | | </div> |
| | | <!-- 累计数据趋势 --> |
| | | <!-- <div class="row row-3"> |
| | | <div class="panel-card card-10"> |
| | | <div class="panel-title">累计销量趋势</div> |
| | | <div class="chart-container"> |
| | | <div ref="cumulativeSalesVolumeChart" |
| | | style="width: 100%; height: 100%;"></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 === 'year' }" |
| | | @click="handleCustomerTimeDimensionChange('year')">年</span> |
| | | <span class="tab-item" |
| | | :class="{ active: customerTimeDimension === 'month' }" |
| | | @click="handleCustomerTimeDimensionChange('month')">月</span> |
| | | </div> |
| | | <div class="bi-panel-body"> |
| | | <div class="chart-unit-row chart-unit-single"> |
| | | <span>单位:家</span> |
| | | </div> |
| | | <div class="panel-card card-11"> |
| | | <div class="panel-title">累计销售金额趋势</div> |
| | | <div class="chart-container"> |
| | | <div ref="cumulativeSalesAmountChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </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: boardTimeDimension === 'year' }" |
| | | @click="handleBoardTimeDimensionChange('year')">年</span> |
| | | <span class="tab-item" |
| | | :class="{ active: boardTimeDimension === 'month' }" |
| | | @click="handleBoardTimeDimensionChange('month')">月</span> |
| | | </div> |
| | | <div class="panel-tabs2"> |
| | | <span class="tab-item" |
| | | :class="{ active: boardProductType === '砌块' }" |
| | | @click="handleBoardProductTypeChange('砌块')">砌块</span> |
| | | <span class="tab-item" |
| | | :class="{ active: boardProductType === '板材' }" |
| | | @click="handleBoardProductTypeChange('板材')">板材</span> |
| | | </div> |
| | | <div class="bi-panel-body"> |
| | | <div class="chart-filter-tabs"> |
| | | <span v-for="area in salesAreas" |
| | | :key="area" |
| | | class="cf-tab" |
| | | :class="{ active: boardSelectedArea === area }" |
| | | @click="handleBoardAreaChange(area)">{{ area }}</span> |
| | | </div> |
| | | </div> --> |
| | | <!-- 图表区域和表格 --> |
| | | <div class="row row-4"> |
| | | <!-- 左边:详细数据表格 --> |
| | | <div class="panel-card card-9" |
| | | style="flex: 2;"> |
| | | <div class="panel-title">销售统计详细数据</div> |
| | | <div class="table-container"> |
| | | <el-table :data="tableData" |
| | | style="width: 100%"> |
| | | <el-table-column prop="productType" |
| | | label="产品类型" |
| | | width="120" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getProductTypeType(scope.row.productType)"> |
| | | {{ scope.row.productType }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="salesArea" |
| | | label="销售区域" |
| | | width="120" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getSalesAreaType(scope.row.salesArea)"> |
| | | {{ scope.row.salesArea }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="period" |
| | | label="统计周期" |
| | | width="120" /> |
| | | <el-table-column prop="salesVolume" |
| | | label="销量(立方米)" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.salesVolume }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="salesAmount" |
| | | label="销售金额(万元)" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.salesAmount }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="newCustomers" |
| | | label="新增客户(个)" |
| | | width="150" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.newCustomers }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="totalCustomers" |
| | | label="合计客户(个)" |
| | | width="150" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.totalCustomers }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | <!-- 右边:产品类型分布和销售区域分布 --> |
| | | <div class="chart-column" |
| | | style="flex: 1; display: flex; flex-direction: column; gap: 20px;"> |
| | | <div class="panel-card card-7" |
| | | style="flex: 1;"> |
| | | <div class="panel-title">产品类型分布</div> |
| | | <div class="chart-container"> |
| | | <div ref="productTypeChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | <div class="scroll-table-container"> |
| | | <table class="scroll-table"> |
| | | <thead> |
| | | <tr> |
| | | <th>排名</th> |
| | | <th>年月</th> |
| | | <th>销售区</th> |
| | | <th>销售额(万元)</th> |
| | | </tr> |
| | | </thead> |
| | | <div class="scroll-table-content"> |
| | | <tbody ref="boardTableBody"> |
| | | <tr :class="item.sort % 2 === 0 ? 'evenTableTr' : 'oddTableTr'" |
| | | v-for="(item, index) in boardSalesData" |
| | | :key="item.period + item.area + index"> |
| | | <td>{{ item.sort }}</td> |
| | | <td>{{ item.period }}</td> |
| | | <td>{{ item.area }}</td> |
| | | <td>{{ item.sales }}</td> |
| | | </tr> |
| | | </tbody> |
| | | </div> |
| | | </div> |
| | | <div class="panel-card card-8" |
| | | style="flex: 1;"> |
| | | <div class="panel-title">销售区域分布</div> |
| | | <div class="chart-container"> |
| | | <div ref="salesAreaChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | </table> |
| | | </div> |
| | | <div class="panel-summary-row"> |
| | | <div class="summary-label">合计</div> |
| | | <div class="summary-value2">127384 万元</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | import { useRouter } from "vue-router"; |
| | | import * as echarts from "echarts"; |
| | | import dayjs from "dayjs"; |
| | | import PanelHeader from "@/views/reportAnalysis/PSIDataAnalysis/components/PanelHeader.vue"; |
| | | import { findAllQualifiedStockOutRecordTypeOptions } from "../../../api/basicData/enum"; |
| | | |
| | | 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 salesAmountChart = ref(null); |
| | | const productTypeChart = ref(null); |
| | | const salesAreaChart = ref(null); |
| | | const productTypeTrendChart = ref(null); |
| | | const blockTableBody = ref(null); |
| | | const boardTableBody = ref(null); |
| | | |
| | | // 选择器数据 |
| | | const blockTimeDimension = ref("year"); |
| | | const blockSelectedArea = ref("全部"); |
| | | const blockProductType = ref("砌块"); |
| | | const boardTimeDimension = ref("year"); |
| | | const boardSelectedArea = ref("全部"); |
| | | const boardProductType = ref("板材"); |
| | | const customerTimeDimension = ref("year"); |
| | | |
| | | 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 salesAmountChartInstance = null; |
| | | let productTypeChartInstance = null; |
| | | let salesAreaChartInstance = null; |
| | | let productTypeTrendChartInstance = null; |
| | | let cumulativeSalesVolumeChartInstance = null; |
| | | let cumulativeSalesAmountChartInstance = null; |
| | | |
| | |
| | | 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 totalSalesAmount = ref(1299); |
| | | |
| | | const newCustomerCount = computed(() => { |
| | | return filteredData.value.reduce((sum, item) => sum + item.newCustomers, 0); |
| | |
| | | }); |
| | | 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 salesVolumeChartOption = computed(() => { |
| | | // 按周期分组 |
| | | const periodMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!periodMap[item.period]) { |
| | | periodMap[item.period] = 0; |
| | | // 为每个销售区生成数据 |
| | | const salesAreas = [ |
| | | "全部", |
| | | "A销售区", |
| | | "B销售区", |
| | | "C销售区", |
| | | "D销售区", |
| | | "E销售区", |
| | | ]; |
| | | const colors = [ |
| | | "#00A4ED", |
| | | "#34D8F7", |
| | | "#4A8BFF", |
| | | "#8A6BFF", |
| | | "#C8C447", |
| | | "#FF6B6B", |
| | | ]; |
| | | const year = 2024; |
| | | const periodType = blockTimeDimension.value; |
| | | |
| | | // 生成时间段 |
| | | let periods = []; |
| | | if (periodType === "year") { |
| | | // 年度数据:12个月 |
| | | for (let month = 1; month <= 12; month++) { |
| | | periods.push(`${year}-${month.toString().padStart(2, "0")}`); |
| | | } |
| | | periodMap[item.period] += item.salesVolume; |
| | | } 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")}` |
| | | ); |
| | | } |
| | | } |
| | | |
| | | // 为每个销售区生成数据 |
| | | const 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" }, |
| | | ]), |
| | | }, |
| | | }; |
| | | }); |
| | | |
| | | const periods = Object.keys(periodMap).sort(); |
| | | const values = periods.map(period => periodMap[period]); |
| | | |
| | | return { |
| | | backgroundColor: "transparent", |
| | | tooltip: { |
| | | trigger: "axis", |
| | | formatter: "{b}: {c} 立方米", |
| | | 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(9), |
| | | }, |
| | | 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: "销量(立方米)", |
| | | }, |
| | | series: [ |
| | | { |
| | | data: values, |
| | | type: "line", |
| | | smooth: true, |
| | | lineStyle: { |
| | | width: 3, |
| | | }, |
| | | itemStyle: { |
| | | color: "#409EFF", |
| | | }, |
| | | 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 periodMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!periodMap[item.period]) { |
| | | periodMap[item.period] = 0; |
| | | // 为每个销售区生成数据 |
| | | const salesAreas = [ |
| | | "全部", |
| | | "A销售区", |
| | | "B销售区", |
| | | "C销售区", |
| | | "D销售区", |
| | | "E销售区", |
| | | ]; |
| | | const colors = [ |
| | | "#00A4ED", |
| | | "#34D8F7", |
| | | "#4A8BFF", |
| | | "#8A6BFF", |
| | | "#C8C447", |
| | | "#FF6B6B", |
| | | ]; |
| | | const year = 2024; |
| | | const periodType = boardTimeDimension.value; |
| | | |
| | | // 生成时间段 |
| | | let periods = []; |
| | | if (periodType === "year") { |
| | | // 年度数据:12个月 |
| | | for (let month = 1; month <= 12; month++) { |
| | | periods.push(`${year}-${month.toString().padStart(2, "0")}`); |
| | | } |
| | | periodMap[item.period] += item.salesAmount; |
| | | } 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")}` |
| | | ); |
| | | } |
| | | } |
| | | |
| | | // 为每个销售区生成数据 |
| | | const series = salesAreas.map((area, index) => { |
| | | const data = periods.map(() => { |
| | | return periodType === "year" |
| | | ? Math.floor(Math.random() * 50000) + 80000 |
| | | : Math.floor(Math.random() * 5000) + 2000; |
| | | }); |
| | | |
| | | return { |
| | | name: area, |
| | | data: data, |
| | | type: "bar", |
| | | smooth: true, |
| | | lineStyle: { width: getResponsiveValue(3), color: colors[index] }, |
| | | itemStyle: { color: colors[index] }, |
| | | areaStyle: { |
| | | opacity: 0.2, |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: colors[index] + "80" }, |
| | | { offset: 1, color: colors[index] + "00" }, |
| | | ]), |
| | | }, |
| | | }; |
| | | }); |
| | | |
| | | const periods = Object.keys(periodMap).sort(); |
| | | const values = periods.map(period => periodMap[period]); |
| | | |
| | | return { |
| | | backgroundColor: "transparent", |
| | | tooltip: { |
| | | trigger: "axis", |
| | | formatter: "{b}: {c} 万元", |
| | | 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(9), |
| | | }, |
| | | 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: "销售金额(万元)", |
| | | }, |
| | | series: [ |
| | | { |
| | | data: values, |
| | | type: "bar", |
| | | itemStyle: { |
| | | color: "#67C23A", |
| | | }, |
| | | 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 typeMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!typeMap[item.productType]) { |
| | | typeMap[item.productType] = 0; |
| | | } |
| | | typeMap[item.productType] += item.salesVolume; |
| | | }); |
| | | |
| | | const types = Object.keys(typeMap); |
| | | const values = types.map(type => typeMap[type]); |
| | | 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 { |
| | | tooltip: { |
| | | trigger: "item", |
| | | formatter: "{b}: {c} 立方米 ({d}%)", |
| | | }, |
| | | series: [ |
| | | { |
| | | type: "pie", |
| | | radius: "60%", |
| | | data: types.map((type, index) => ({ |
| | | name: type, |
| | | value: values[index], |
| | | })), |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowOffsetX: 0, |
| | | shadowColor: "rgba(0, 0, 0, 0.5)", |
| | | }, |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // 销售区域分布图表配置 |
| | | const salesAreaChartOption = computed(() => { |
| | | // 按销售区域分组 |
| | | const areaMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!areaMap[item.salesArea]) { |
| | | areaMap[item.salesArea] = 0; |
| | | } |
| | | areaMap[item.salesArea] += item.salesVolume; |
| | | }); |
| | | |
| | | const areas = Object.keys(areaMap); |
| | | const values = areas.map(area => areaMap[area]); |
| | | |
| | | return { |
| | | tooltip: { |
| | | trigger: "item", |
| | | formatter: "{b}: {c} 立方米 ({d}%)", |
| | | }, |
| | | series: [ |
| | | { |
| | | type: "pie", |
| | | radius: "60%", |
| | | data: areas.map((area, index) => ({ |
| | | name: area, |
| | | value: values[index], |
| | | })), |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowOffsetX: 0, |
| | | shadowColor: "rgba(0, 0, 0, 0.5)", |
| | | }, |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // 累计销量趋势图表配置 |
| | | 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 { |
| | | 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: "category", |
| | | data: periods, |
| | | type: "value", |
| | | axisLine: { show: false }, |
| | | axisLabel: { color: "#B8C8E0", fontSize: getResponsiveValue(11) }, |
| | | splitLine: { lineStyle: { color: "rgba(184,200,224,0.12)" } }, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "累计销量(立方米)", |
| | | 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, |
| | | type: "line", |
| | | smooth: true, |
| | | areaStyle: { |
| | | opacity: 0.3, |
| | | }, |
| | | itemStyle: { |
| | | color: "#E6A23C", |
| | | color: params => barColors[params.dataIndex] || "#00A4ED", |
| | | borderRadius: [ |
| | | getResponsiveValue(6), |
| | | getResponsiveValue(6), |
| | | getResponsiveValue(6), |
| | | getResponsiveValue(6), |
| | | ], |
| | | }, |
| | | lineStyle: { |
| | | width: 3, |
| | | label: { |
| | | show: false, |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // 累计销售金额趋势图表配置 |
| | | 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]); |
| | | // 销售区域销量图表配置(横向柱状) |
| | | 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", |
| | | formatter: "{b}: {c} 万元", |
| | | 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: "category", |
| | | data: periods, |
| | | type: "value", |
| | | axisLine: { show: false }, |
| | | axisLabel: { color: "#B8C8E0", fontSize: getResponsiveValue(11) }, |
| | | splitLine: { lineStyle: { color: "rgba(184,200,224,0.12)" } }, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "累计销售金额(万元)", |
| | | type: "category", |
| | | data: areas, |
| | | axisTick: { show: false }, |
| | | axisLine: { show: false }, |
| | | axisLabel: { |
| | | color: "#B8C8E0", |
| | | fontSize: getResponsiveValue(11), |
| | | margin: getResponsiveValue(8), |
| | | }, |
| | | }, |
| | | series: [ |
| | | { |
| | | data: values, |
| | | name: "销量(立方米)", |
| | | type: "bar", |
| | | barWidth: getResponsiveValue(14), |
| | | data: values, |
| | | itemStyle: { |
| | | color: "#F56C6C", |
| | | color: params => barColors[params.dataIndex] || "#00A4ED", |
| | | borderRadius: [ |
| | | getResponsiveValue(6), |
| | | getResponsiveValue(6), |
| | | getResponsiveValue(6), |
| | | getResponsiveValue(6), |
| | | ], |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // 方法 |
| | | const goBack = () => { |
| | | router.back(); |
| | | }; |
| | | // 新增客户趋势图表配置(按销售区和年月维度) |
| | | const productTypeTrendChartOption = computed(() => { |
| | | // 为每个销售区生成数据 |
| | | const salesAreas = [ |
| | | "全部", |
| | | "A销售区", |
| | | "B销售区", |
| | | "C销售区", |
| | | "D销售区", |
| | | "E销售区", |
| | | ]; |
| | | const colors = [ |
| | | "#00A4ED", |
| | | "#34D8F7", |
| | | "#4A8BFF", |
| | | "#8A6BFF", |
| | | "#C8C447", |
| | | "#FF6B6B", |
| | | ]; |
| | | const year = 2024; |
| | | const periodType = customerTimeDimension.value; |
| | | |
| | | const handleDateChange = () => { |
| | | // 处理日期变化 |
| | | updateCharts(); |
| | | }; |
| | | // 生成时间段 |
| | | let periods = []; |
| | | 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")}` |
| | | ); |
| | | } |
| | | } |
| | | |
| | | const handleFilterChange = () => { |
| | | // 处理筛选条件变化 |
| | | updateCharts(); |
| | | // 为每个销售区生成数据 |
| | | const 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(9), |
| | | }, |
| | | 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); |
| | | }; |
| | | |
| | | // 初始化图表 |
| | |
| | | salesAreaChartInstance = echarts.init(salesAreaChart.value); |
| | | } |
| | | |
| | | // 初始化新增客户趋势图表 |
| | | if (productTypeTrendChart.value && !productTypeTrendChartInstance) { |
| | | productTypeTrendChartInstance = echarts.init(productTypeTrendChart.value); |
| | | } |
| | | |
| | | // 初始化累计销量趋势图表 |
| | | if (cumulativeSalesVolumeChart.value && !cumulativeSalesVolumeChartInstance) { |
| | | cumulativeSalesVolumeChartInstance = echarts.init( |
| | |
| | | |
| | | updateCharts(); |
| | | }; |
| | | |
| | | // 更新图表 |
| | | const updateCharts = () => { |
| | | // 更新销量趋势图表 |
| | | if (salesVolumeChartInstance) { |
| | | salesVolumeChartInstance.setOption(salesVolumeChartOption.value); |
| | | salesVolumeChartInstance.setOption( |
| | | JSON.parse(JSON.stringify(salesVolumeChartOption.value)) |
| | | ); |
| | | } |
| | | |
| | | // 更新销售金额趋势图表 |
| | |
| | | salesAreaChartInstance.setOption(salesAreaChartOption.value); |
| | | } |
| | | |
| | | // 更新新增客户趋势图表 |
| | | if (productTypeTrendChartInstance) { |
| | | productTypeTrendChartInstance.setOption(productTypeTrendChartOption.value); |
| | | } |
| | | |
| | | // 更新累计销量趋势图表 |
| | | if (cumulativeSalesVolumeChartInstance) { |
| | | cumulativeSalesVolumeChartInstance.setOption( |
| | |
| | | } |
| | | }; |
| | | |
| | | // 表格动画控制 |
| | | let blockScrollTimer = null; |
| | | let boardScrollTimer = null; |
| | | let blockCurrentIndex = 0; |
| | | let boardCurrentIndex = 0; |
| | | |
| | | const startBlockTableScroll = () => { |
| | | if (blockScrollTimer) { |
| | | clearInterval(blockScrollTimer); |
| | | } |
| | | |
| | | const scrollTable = () => { |
| | | if (!blockTableBody.value || blockSalesData.value.length === 0) 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)"; |
| | | |
| | | const firstItem = blockSalesData.value[0]; |
| | | blockSalesData.value.shift(); |
| | | blockSalesData.value.push(firstItem); |
| | | }, 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; |
| | | } |
| | | }; |
| | | |
| | | // 处理时间维度选择 |
| | | const handleBlockTimeDimensionChange = dimension => { |
| | | blockTimeDimension.value = dimension; |
| | | generateBlockSalesData(); |
| | | }; |
| | | |
| | | const handleBoardTimeDimensionChange = dimension => { |
| | | boardTimeDimension.value = dimension; |
| | | generateBoardSalesData(); |
| | | }; |
| | | |
| | | // 处理产品类型选择 |
| | | const handleBlockProductTypeChange = type => { |
| | | blockProductType.value = type; |
| | | generateBlockSalesData(); |
| | | }; |
| | | |
| | | const handleBoardProductTypeChange = type => { |
| | | boardProductType.value = type; |
| | | generateBoardSalesData(); |
| | | }; |
| | | |
| | | // 处理销售区选择 |
| | | const handleBlockAreaChange = area => { |
| | | blockSelectedArea.value = area; |
| | | generateBlockSalesData(); |
| | | }; |
| | | |
| | | const handleBoardAreaChange = area => { |
| | | boardSelectedArea.value = area; |
| | | generateBoardSalesData(); |
| | | }; |
| | | |
| | | // 处理新增客户趋势时间维度切换 |
| | | const handleCustomerTimeDimensionChange = dimension => { |
| | | customerTimeDimension.value = dimension; |
| | | updateCharts(); |
| | | }; |
| | | |
| | | // 生成砌块销售数据 |
| | | 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 (salesAreaChartInstance) { |
| | | salesAreaChartInstance.resize(); |
| | | } |
| | | if (productTypeTrendChartInstance) { |
| | | productTypeTrendChartInstance.resize(); |
| | | } |
| | | if (cumulativeSalesVolumeChartInstance) { |
| | | cumulativeSalesVolumeChartInstance.resize(); |
| | | } |
| | |
| | | |
| | | // 生命周期 |
| | | onMounted(() => { |
| | | // 启动顶部栏时间刷新 |
| | | if (!timeTicker) { |
| | | timeTicker = setInterval(() => { |
| | | now.value = dayjs(); |
| | | }, 1000); |
| | | } |
| | | |
| | | // 设置默认日期范围为最近3个月 |
| | | const endDate = dayjs(); |
| | | const startDate = endDate.subtract(3, "month"); |
| | |
| | | endDate.format("YYYY-MM-DD"), |
| | | ]; |
| | | |
| | | // 生成初始数据 |
| | | generateBlockSalesData(); |
| | | generateBoardSalesData(); |
| | | |
| | | // 等待DOM更新后初始化图表 |
| | | nextTick(() => { |
| | | initCharts(); |
| | | // 启动表格滚动动画 |
| | | startBlockTableScroll(); |
| | | startBoardTableScroll(); |
| | | }); |
| | | |
| | | // 添加窗口大小变化监听 |
| | | window.addEventListener("resize", handleResize); |
| | | document.addEventListener("fullscreenchange", handleFullscreenChange); |
| | | }); |
| | | |
| | | // 获取产品类型标签类型 |
| | |
| | | |
| | | // 组件卸载时销毁图表实例 |
| | | onBeforeUnmount(() => { |
| | | if (timeTicker) { |
| | | clearInterval(timeTicker); |
| | | timeTicker = null; |
| | | } |
| | | |
| | | if (salesVolumeChartInstance) { |
| | | salesVolumeChartInstance.dispose(); |
| | | } |
| | |
| | | if (salesAreaChartInstance) { |
| | | salesAreaChartInstance.dispose(); |
| | | } |
| | | |
| | | if (productTypeTrendChartInstance) { |
| | | productTypeTrendChartInstance.dispose(); |
| | | } |
| | | if (cumulativeSalesVolumeChartInstance) { |
| | | cumulativeSalesVolumeChartInstance.dispose(); |
| | | } |
| | |
| | | |
| | | // 移除窗口大小变化监听 |
| | | window.removeEventListener("resize", handleResize); |
| | | document.removeEventListener("fullscreenchange", handleFullscreenChange); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | /* 外部容器 - 占据整个视口 */ |
| | | .sales-statistics-container { |
| | | position: relative; |
| | | width: 100%; |
| | | /* 页面在常规布局下(有顶栏)默认减去 84px,避免内容被裁切 */ |
| | | min-height: calc(100vh - 84px); |
| | | background-color: #f5f7fa; |
| | | min-height: calc(100vh - 8.4vh); |
| | | overflow: hidden; |
| | | color: #b8c8e0; |
| | | background: #041026; |
| | | } |
| | | |
| | | /* 内部内容区域 - 自适应宽度 */ |
| | | .data-dashboard { |
| | | .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%; |
| | | min-height: 100%; |
| | | background-color: #ffffff; |
| | | box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); |
| | | object-fit: cover; |
| | | z-index: 0; |
| | | pointer-events: none; |
| | | } |
| | | |
| | | .filter-area { |
| | | padding: 20px; |
| | | background-color: #ffffff; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | display: flex; |
| | | gap: 40px; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .filter-section { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .filter-label { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .dashboard-content { |
| | | .bi-topbar-content { |
| | | position: relative; |
| | | z-index: 1; |
| | | width: 100%; |
| | | padding: 0 2.8vh; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 20px; |
| | | padding: 20px; |
| | | min-height: 800px; |
| | | overflow: hidden; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | /* 行布局 */ |
| | | .row { |
| | | .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; |
| | | gap: 20px; |
| | | align-items: stretch; |
| | | align-items: center; |
| | | gap: 0.8vh; |
| | | color: rgba(208, 231, 255, 0.85); |
| | | font-size: 1.3vh; |
| | | } |
| | | |
| | | /* 第一行:4个指标卡片 */ |
| | | .row-1 { |
| | | height: 180px; |
| | | .status-sun { |
| | | color: #ffd85e; |
| | | text-shadow: 0 0 1vh rgba(255, 216, 94, 0.8); |
| | | font-size: 1.3vh; |
| | | line-height: 1; |
| | | } |
| | | |
| | | /* 第二行:2个趋势图表 */ |
| | | .row-2 { |
| | | height: 350px; |
| | | } |
| | | |
| | | /* 第三行:累计数据趋势 */ |
| | | .row-3 { |
| | | height: 350px; |
| | | } |
| | | |
| | | /* 第四行:表格和图表 */ |
| | | .row-4 { |
| | | height: 600px; |
| | | } |
| | | |
| | | /* 卡片样式 */ |
| | | .panel-card { |
| | | background-color: #ffffff; |
| | | border-radius: 8px; |
| | | border: 1px solid #e4e7ed; |
| | | overflow: hidden; |
| | | display: flex; |
| | | flex-direction: column; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .panel-card:hover { |
| | | box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); |
| | | transform: translateY(-2px); |
| | | } |
| | | |
| | | /* 卡片布局 */ |
| | | .card-1 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-2 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-3 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-4 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-5 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-6 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-7 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-8 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-9 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-10 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-11 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .panel-title { |
| | | padding: 15px 20px; |
| | | font-size: 16px; |
| | | .bi-topbar-meta { |
| | | position: absolute; |
| | | right: 5.2vh; |
| | | /* top: 1.6vh; */ |
| | | font-size: 1.2vh; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | background-color: #fafafa; |
| | | letter-spacing: 0.05vh; |
| | | color: rgba(208, 231, 255, 0.85); |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 1vh; |
| | | } |
| | | |
| | | .card-1 .panel-title { |
| | | border-left: 4px solid #409eff; |
| | | .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; |
| | | } |
| | | |
| | | .card-2 .panel-title { |
| | | border-left: 4px solid #67c23a; |
| | | .fullscreen-btn:hover { |
| | | background: rgba(0, 164, 237, 0.24); |
| | | box-shadow: 0 0 1.2vh rgba(0, 164, 237, 0.3); |
| | | } |
| | | |
| | | .card-3 .panel-title { |
| | | border-left: 4px solid #e6a23c; |
| | | .bi-topbar-sep { |
| | | opacity: 0.7; |
| | | } |
| | | |
| | | .card-4 .panel-title { |
| | | border-left: 4px solid #f56c6c; |
| | | /* 主体网格布局 */ |
| | | .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; |
| | | } |
| | | |
| | | .card-5 .panel-title { |
| | | border-left: 4px solid #409eff; |
| | | .sales-statistics-container.is-fullscreen .bi-dashboard-grid { |
| | | height: calc(100vh - 5.8vh); |
| | | } |
| | | |
| | | .card-6 .panel-title { |
| | | border-left: 4px solid #67c23a; |
| | | .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; |
| | | } |
| | | |
| | | .card-7 .panel-title { |
| | | border-left: 4px solid #e6a23c; |
| | | .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); |
| | | } |
| | | |
| | | .card-8 .panel-title { |
| | | border-left: 4px solid #f56c6c; |
| | | .panel-tabs, |
| | | .panel-tabs2 { |
| | | position: absolute; |
| | | top: 0.8vh; |
| | | display: flex; |
| | | gap: 0.6vh; |
| | | z-index: 4; |
| | | } |
| | | |
| | | .card-9 .panel-title { |
| | | border-left: 4px solid #409eff; |
| | | .panel-tabs { |
| | | right: 1.2vh; |
| | | } |
| | | |
| | | .card-10 .panel-title { |
| | | border-left: 4px solid #67c23a; |
| | | .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; |
| | | } |
| | | |
| | | .card-11 .panel-title { |
| | | border-left: 4px solid #e6a23c; |
| | | .tab-item.active { |
| | | color: #ffffff; |
| | | border-color: rgba(0, 164, 237, 0.65); |
| | | background: rgba(0, 164, 237, 0.22); |
| | | } |
| | | |
| | | .chart-container { |
| | | .bi-panel-body { |
| | | flex: 1; |
| | | padding: 20px; |
| | | padding: 0.8vh 1vh; |
| | | position: relative; |
| | | } |
| | | |
| | | .table-container { |
| | | flex: 1; |
| | | padding: 20px; |
| | | .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: 5.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; |
| | | } |
| | | |
| | | .stats-grid { |
| | | flex: 1; |
| | | padding: 15px; |
| | | .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; |
| | | justify-content: center; |
| | | background-color: #081843; |
| | | } |
| | | |
| | | .stat-item { |
| | | background-color: #fafafa; |
| | | border-radius: 8px; |
| | | padding: 15px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border: 1px solid #e4e7ed; |
| | | min-height: 80px; |
| | | width: 100%; |
| | | .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; |
| | | } |
| | | |
| | | .stat-value { |
| | | font-size: 24px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 5px; |
| | | .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; |
| | | } |
| | | |
| | | .sales-volume-color { |
| | | color: #409eff; |
| | | text-shadow: 0 2px 4px rgba(64, 158, 255, 0.3); |
| | | } |
| | | |
| | | .sales-amount-color { |
| | | color: #67c23a; |
| | | text-shadow: 0 2px 4px rgba(103, 194, 58, 0.3); |
| | | } |
| | | |
| | | .new-customer-color { |
| | | color: #e6a23c; |
| | | text-shadow: 0 2px 4px rgba(230, 162, 60, 0.3); |
| | | } |
| | | |
| | | .total-customer-color { |
| | | color: #f56c6c; |
| | | text-shadow: 0 2px 4px rgba(245, 108, 108, 0.3); |
| | | } |
| | | |
| | | .stat-unit { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-bottom: 3px; |
| | | } |
| | | |
| | | .stat-change { |
| | | font-size: 12px; |
| | | color: #67c23a; |
| | | } |
| | | |
| | | /* 表格样式 */ |
| | | :deep(.el-table) { |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | :deep(.el-table th) { |
| | | background-color: #fafafa; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | :deep(.el-table tr:hover > td) { |
| | | background-color: #ecf5ff; |
| | | } |
| | | |
| | | .data-value { |
| | | font-weight: bold; |
| | | color: #409eff; |
| | | } |
| | | |
| | | /* 下拉选择框样式 */ |
| | | :deep(.el-select) { |
| | | width: 100%; |
| | | } |
| | | |
| | | :deep(.el-date-picker) { |
| | | width: 100%; |
| | | } |
| | | </style> |
| | | </style> |