| | |
| | | <!-- <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;" |
| | | /> |
| | | <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> |
| | | <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 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 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 v-model="statDimension" |
| | | placeholder="请选择统计维度" |
| | | @change="handleFilterChange" |
| | | style="width: 160px;"> |
| | | <el-option label="月度" |
| | | value="month" /> |
| | | <el-option label="年度" |
| | | value="year" /> |
| | | </el-select> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="dashboard-content"> |
| | | <!-- 核心指标卡片 --> |
| | | <div class="row row-1"> |
| | |
| | | <div class="panel-title">合计销量</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value">{{ totalSalesVolume }}</div> |
| | | <div class="stat-value sales-volume-color">{{ totalSalesVolume }}</div> |
| | | <div class="stat-unit">立方米</div> |
| | | <div class="stat-change">{{ salesVolumeChange }}%</div> |
| | | </div> |
| | |
| | | <div class="panel-title">销售金额</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value">{{ totalSalesAmount }}</div> |
| | | <div class="stat-value sales-amount-color">{{ totalSalesAmount }}</div> |
| | | <div class="stat-unit">万元</div> |
| | | <div class="stat-change">{{ salesAmountChange }}%</div> |
| | | </div> |
| | |
| | | <div class="panel-title">新增客户</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value">{{ newCustomerCount }}</div> |
| | | <div class="stat-value new-customer-color">{{ newCustomerCount }}</div> |
| | | <div class="stat-unit">个</div> |
| | | <div class="stat-change">{{ customerCountChange }}%</div> |
| | | </div> |
| | |
| | | <div class="panel-title">合计客户</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value">{{ totalCustomerCount }}</div> |
| | | <div class="stat-value total-customer-color">{{ totalCustomerCount }}</div> |
| | | <div class="stat-unit">个</div> |
| | | <div class="stat-change">{{ totalCustomerChange }}%</div> |
| | | </div> |
| | | </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 ref="salesVolumeChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </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 ref="salesAmountChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 累计数据趋势 --> |
| | | <div class="row row-3"> |
| | | <!-- <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 ref="cumulativeSalesVolumeChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </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 ref="cumulativeSalesAmountChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | </div> --> |
| | | <!-- 图表区域和表格 --> |
| | | <div class="row row-4"> |
| | | <!-- 左边:详细数据表格 --> |
| | | <div class="panel-card card-9" style="flex: 2;"> |
| | | <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" /> |
| | | <el-table-column prop="salesArea" label="销售区域" width="120" /> |
| | | <el-table-column prop="period" label="统计周期" width="120" /> |
| | | <el-table-column prop="salesVolume" label="销量(立方米)" /> |
| | | <el-table-column prop="salesAmount" label="销售金额(万元)" /> |
| | | <el-table-column prop="newCustomers" label="新增客户(个)" width="150" /> |
| | | <el-table-column prop="totalCustomers" label="合计客户(个)" width="150" /> |
| | | <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="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 ref="productTypeChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | <div class="panel-card card-8" style="flex: 1;"> |
| | | <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 ref="salesAreaChart" |
| | | style="width: 100%; height: 100%;"></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 { |
| | | ref, |
| | | computed, |
| | | onMounted, |
| | | onBeforeUnmount, |
| | | watch, |
| | | nextTick, |
| | | } from "vue"; |
| | | import { useRouter } from "vue-router"; |
| | | import * as echarts from "echarts"; |
| | | import dayjs from "dayjs"; |
| | | |
| | | const router = useRouter(); |
| | | const router = useRouter(); |
| | | |
| | | // 筛选条件 |
| | | const dateRange = ref([]); |
| | | const productType = ref(''); |
| | | const salesArea = ref(''); |
| | | const statDimension = ref('month'); |
| | | // 筛选条件 |
| | | 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 cumulativeSalesVolumeChart = ref(null); |
| | | const cumulativeSalesAmountChart = ref(null); |
| | | // 图表引用 |
| | | const salesVolumeChart = ref(null); |
| | | const salesAmountChart = ref(null); |
| | | const productTypeChart = ref(null); |
| | | const salesAreaChart = ref(null); |
| | | const cumulativeSalesVolumeChart = ref(null); |
| | | const cumulativeSalesAmountChart = ref(null); |
| | | |
| | | // 图表实例 |
| | | let salesVolumeChartInstance = null; |
| | | let salesAmountChartInstance = null; |
| | | let productTypeChartInstance = null; |
| | | let salesAreaChartInstance = null; |
| | | let cumulativeSalesVolumeChartInstance = null; |
| | | let cumulativeSalesAmountChartInstance = null; |
| | | // 图表实例 |
| | | let salesVolumeChartInstance = null; |
| | | let salesAmountChartInstance = null; |
| | | let productTypeChartInstance = null; |
| | | let salesAreaChartInstance = 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 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]; |
| | | |
| | | // 计算属性 |
| | | 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; |
| | | // 按产品类型筛选 |
| | | 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; |
| | | }); |
| | | return Object.values(customerMap).reduce((sum, count) => sum + count, 0); |
| | | }); |
| | | |
| | | // 变化率计算(模拟) |
| | | const salesVolumeChange = ref('+5.2'); |
| | | const salesAmountChange = ref('+7.8'); |
| | | const customerCountChange = ref('+3.5'); |
| | | const totalCustomerChange = ref('+2.1'); |
| | | // 核心指标计算 |
| | | const totalSalesVolume = computed(() => { |
| | | return filteredData.value.reduce((sum, item) => sum + item.salesVolume, 0); |
| | | }); |
| | | |
| | | // 表格数据 |
| | | 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; |
| | | |
| | | 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 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 periodMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!periodMap[item.period]) { |
| | | periodMap[item.period] = 0; |
| | | } |
| | | periodMap[item.period] += item.salesVolume; |
| | | }); |
| | | |
| | | const periods = Object.keys(periodMap).sort(); |
| | | const values = periods.map(period => periodMap[period]); |
| | | |
| | | return { |
| | | ...item, |
| | | cumulativeSalesVolume, |
| | | cumulativeSalesAmount, |
| | | cumulativeNewCustomers |
| | | tooltip: { |
| | | trigger: "axis", |
| | | formatter: "{b}: {c} 立方米", |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: periods, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "销量(立方米)", |
| | | }, |
| | | series: [ |
| | | { |
| | | data: values, |
| | | type: "line", |
| | | smooth: true, |
| | | lineStyle: { |
| | | width: 3, |
| | | }, |
| | | itemStyle: { |
| | | color: "#409EFF", |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | }); |
| | | |
| | | // 销量趋势图表配置 |
| | | const salesVolumeChartOption = computed(() => { |
| | | // 按周期分组 |
| | | const periodMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!periodMap[item.period]) { |
| | | periodMap[item.period] = 0; |
| | | } |
| | | periodMap[item.period] += item.salesVolume; |
| | | }); |
| | | |
| | | 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, |
| | | lineStyle: { |
| | | width: 3 |
| | | // 销售金额趋势图表配置 |
| | | const salesAmountChartOption = computed(() => { |
| | | // 按周期分组 |
| | | const periodMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!periodMap[item.period]) { |
| | | periodMap[item.period] = 0; |
| | | } |
| | | periodMap[item.period] += item.salesAmount; |
| | | }); |
| | | |
| | | const periods = Object.keys(periodMap).sort(); |
| | | const values = periods.map(period => periodMap[period]); |
| | | |
| | | return { |
| | | tooltip: { |
| | | trigger: "axis", |
| | | formatter: "{b}: {c} 万元", |
| | | }, |
| | | itemStyle: { |
| | | color: '#409EFF' |
| | | } |
| | | }] |
| | | }; |
| | | }); |
| | | |
| | | // 销售金额趋势图表配置 |
| | | const salesAmountChartOption = computed(() => { |
| | | // 按周期分组 |
| | | const periodMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!periodMap[item.period]) { |
| | | periodMap[item.period] = 0; |
| | | } |
| | | periodMap[item.period] += item.salesAmount; |
| | | }); |
| | | |
| | | 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: '#67C23A' |
| | | } |
| | | }] |
| | | }; |
| | | }); |
| | | |
| | | // 产品类型分布图表配置 |
| | | 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]); |
| | | |
| | | 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 { |
| | | 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 |
| | | xAxis: { |
| | | type: "category", |
| | | data: periods, |
| | | }, |
| | | itemStyle: { |
| | | color: '#E6A23C' |
| | | yAxis: { |
| | | type: "value", |
| | | name: "销售金额(万元)", |
| | | }, |
| | | 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; |
| | | series: [ |
| | | { |
| | | data: values, |
| | | type: "bar", |
| | | itemStyle: { |
| | | color: "#67C23A", |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | 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 productTypeChartOption = computed(() => { |
| | | // 按产品类型分组 |
| | | const typeMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!typeMap[item.productType]) { |
| | | typeMap[item.productType] = 0; |
| | | } |
| | | }] |
| | | }; |
| | | }); |
| | | typeMap[item.productType] += item.salesVolume; |
| | | }); |
| | | |
| | | // 方法 |
| | | const goBack = () => { |
| | | router.back(); |
| | | }; |
| | | const types = Object.keys(typeMap); |
| | | const values = types.map(type => typeMap[type]); |
| | | |
| | | 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 (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 (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 (cumulativeSalesVolumeChartInstance) { |
| | | cumulativeSalesVolumeChartInstance.resize(); |
| | | } |
| | | if (cumulativeSalesAmountChartInstance) { |
| | | cumulativeSalesAmountChartInstance.resize(); |
| | | } |
| | | }; |
| | | |
| | | // 生命周期 |
| | | onMounted(() => { |
| | | // 设置默认日期范围为最近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(); |
| | | 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)", |
| | | }, |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // 添加窗口大小变化监听 |
| | | window.addEventListener('resize', handleResize); |
| | | }); |
| | | |
| | | // 组件卸载时销毁图表实例 |
| | | onBeforeUnmount(() => { |
| | | if (salesVolumeChartInstance) { |
| | | salesVolumeChartInstance.dispose(); |
| | | } |
| | | if (salesAmountChartInstance) { |
| | | salesAmountChartInstance.dispose(); |
| | | } |
| | | if (productTypeChartInstance) { |
| | | productTypeChartInstance.dispose(); |
| | | } |
| | | if (salesAreaChartInstance) { |
| | | salesAreaChartInstance.dispose(); |
| | | } |
| | | if (cumulativeSalesVolumeChartInstance) { |
| | | cumulativeSalesVolumeChartInstance.dispose(); |
| | | } |
| | | if (cumulativeSalesAmountChartInstance) { |
| | | cumulativeSalesAmountChartInstance.dispose(); |
| | | } |
| | | |
| | | // 移除窗口大小变化监听 |
| | | window.removeEventListener('resize', handleResize); |
| | | }); |
| | | // 销售区域分布图表配置 |
| | | 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 { |
| | | 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 (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 (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 (cumulativeSalesVolumeChartInstance) { |
| | | cumulativeSalesVolumeChartInstance.resize(); |
| | | } |
| | | if (cumulativeSalesAmountChartInstance) { |
| | | cumulativeSalesAmountChartInstance.resize(); |
| | | } |
| | | }; |
| | | |
| | | // 生命周期 |
| | | onMounted(() => { |
| | | // 设置默认日期范围为最近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); |
| | | }); |
| | | |
| | | // 获取产品类型标签类型 |
| | | 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 (salesVolumeChartInstance) { |
| | | salesVolumeChartInstance.dispose(); |
| | | } |
| | | if (salesAmountChartInstance) { |
| | | salesAmountChartInstance.dispose(); |
| | | } |
| | | if (productTypeChartInstance) { |
| | | productTypeChartInstance.dispose(); |
| | | } |
| | | if (salesAreaChartInstance) { |
| | | salesAreaChartInstance.dispose(); |
| | | } |
| | | if (cumulativeSalesVolumeChartInstance) { |
| | | cumulativeSalesVolumeChartInstance.dispose(); |
| | | } |
| | | if (cumulativeSalesAmountChartInstance) { |
| | | cumulativeSalesAmountChartInstance.dispose(); |
| | | } |
| | | |
| | | // 移除窗口大小变化监听 |
| | | window.removeEventListener("resize", handleResize); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | /* 外部容器 - 占据整个视口 */ |
| | | .sales-statistics-container { |
| | | position: relative; |
| | | width: 100%; |
| | | /* 页面在常规布局下(有顶栏)默认减去 84px,避免内容被裁切 */ |
| | | min-height: calc(100vh - 84px); |
| | | background-color: #f5f7fa; |
| | | overflow: hidden; |
| | | } |
| | | /* 外部容器 - 占据整个视口 */ |
| | | .sales-statistics-container { |
| | | position: relative; |
| | | width: 100%; |
| | | /* 页面在常规布局下(有顶栏)默认减去 84px,避免内容被裁切 */ |
| | | min-height: calc(100vh - 84px); |
| | | background-color: #f5f7fa; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | /* 内部内容区域 - 自适应宽度 */ |
| | | .data-dashboard { |
| | | position: relative; |
| | | width: 100%; |
| | | min-height: 100%; |
| | | background-color: #ffffff; |
| | | box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); |
| | | } |
| | | /* 内部内容区域 - 自适应宽度 */ |
| | | .data-dashboard { |
| | | position: relative; |
| | | width: 100%; |
| | | min-height: 100%; |
| | | background-color: #ffffff; |
| | | box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .filter-area { |
| | | padding: 20px; |
| | | background-color: #ffffff; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | display: flex; |
| | | gap: 40px; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | } |
| | | .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-section { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .filter-label { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | white-space: nowrap; |
| | | } |
| | | .filter-label { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .dashboard-content { |
| | | position: relative; |
| | | z-index: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 20px; |
| | | padding: 20px; |
| | | min-height: 800px; |
| | | overflow: hidden; |
| | | } |
| | | .dashboard-content { |
| | | position: relative; |
| | | z-index: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 20px; |
| | | padding: 20px; |
| | | min-height: 800px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | /* 行布局 */ |
| | | .row { |
| | | display: flex; |
| | | gap: 20px; |
| | | align-items: stretch; |
| | | } |
| | | /* 行布局 */ |
| | | .row { |
| | | display: flex; |
| | | gap: 20px; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | /* 第一行:4个指标卡片 */ |
| | | .row-1 { |
| | | height: 180px; |
| | | } |
| | | /* 第一行:4个指标卡片 */ |
| | | .row-1 { |
| | | height: 180px; |
| | | } |
| | | |
| | | /* 第二行:2个趋势图表 */ |
| | | .row-2 { |
| | | height: 350px; |
| | | } |
| | | /* 第二行:2个趋势图表 */ |
| | | .row-2 { |
| | | height: 350px; |
| | | } |
| | | |
| | | /* 第三行:累计数据趋势 */ |
| | | .row-3 { |
| | | height: 350px; |
| | | } |
| | | /* 第三行:累计数据趋势 */ |
| | | .row-3 { |
| | | height: 350px; |
| | | } |
| | | |
| | | /* 第四行:表格和图表 */ |
| | | .row-4 { |
| | | height: 600px; |
| | | } |
| | | /* 第四行:表格和图表 */ |
| | | .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 { |
| | | 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); |
| | | } |
| | | .panel-card:hover { |
| | | box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); |
| | | transform: translateY(-2px); |
| | | } |
| | | |
| | | /* 卡片布局 */ |
| | | .card-1 { |
| | | flex: 1; |
| | | } |
| | | /* 卡片布局 */ |
| | | .card-1 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-2 { |
| | | flex: 1; |
| | | } |
| | | .card-2 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-3 { |
| | | flex: 1; |
| | | } |
| | | .card-3 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-4 { |
| | | flex: 1; |
| | | } |
| | | .card-4 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-5 { |
| | | flex: 1; |
| | | } |
| | | .card-5 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-6 { |
| | | flex: 1; |
| | | } |
| | | .card-6 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-7 { |
| | | flex: 1; |
| | | } |
| | | .card-7 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-8 { |
| | | flex: 1; |
| | | } |
| | | .card-8 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-9 { |
| | | flex: 1; |
| | | } |
| | | .card-9 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-10 { |
| | | flex: 1; |
| | | } |
| | | .card-10 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-11 { |
| | | flex: 1; |
| | | } |
| | | .card-11 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .panel-title { |
| | | padding: 15px 20px; |
| | | font-size: 16px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | background-color: #fafafa; |
| | | } |
| | | .panel-title { |
| | | padding: 15px 20px; |
| | | font-size: 16px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | background-color: #fafafa; |
| | | } |
| | | |
| | | .chart-container { |
| | | flex: 1; |
| | | padding: 20px; |
| | | } |
| | | .card-1 .panel-title { |
| | | border-left: 4px solid #409eff; |
| | | } |
| | | |
| | | .table-container { |
| | | flex: 1; |
| | | padding: 20px; |
| | | overflow: auto; |
| | | } |
| | | .card-2 .panel-title { |
| | | border-left: 4px solid #67c23a; |
| | | } |
| | | |
| | | .stats-grid { |
| | | flex: 1; |
| | | padding: 15px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | .card-3 .panel-title { |
| | | border-left: 4px solid #e6a23c; |
| | | } |
| | | |
| | | .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%; |
| | | } |
| | | .card-4 .panel-title { |
| | | border-left: 4px solid #f56c6c; |
| | | } |
| | | |
| | | .stat-value { |
| | | font-size: 24px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 5px; |
| | | } |
| | | .card-5 .panel-title { |
| | | border-left: 4px solid #409eff; |
| | | } |
| | | |
| | | .stat-unit { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-bottom: 3px; |
| | | } |
| | | .card-6 .panel-title { |
| | | border-left: 4px solid #67c23a; |
| | | } |
| | | |
| | | .stat-change { |
| | | font-size: 12px; |
| | | color: #67c23a; |
| | | } |
| | | .card-7 .panel-title { |
| | | border-left: 4px solid #e6a23c; |
| | | } |
| | | |
| | | /* 表格样式 */ |
| | | :deep(.el-table) { |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | } |
| | | .card-8 .panel-title { |
| | | border-left: 4px solid #f56c6c; |
| | | } |
| | | |
| | | :deep(.el-table th) { |
| | | background-color: #fafafa; |
| | | font-weight: 500; |
| | | } |
| | | .card-9 .panel-title { |
| | | border-left: 4px solid #409eff; |
| | | } |
| | | |
| | | :deep(.el-table tr:hover > td) { |
| | | background-color: #ecf5ff; |
| | | } |
| | | .card-10 .panel-title { |
| | | border-left: 4px solid #67c23a; |
| | | } |
| | | |
| | | /* 下拉选择框样式 */ |
| | | :deep(.el-select) { |
| | | width: 100%; |
| | | } |
| | | .card-11 .panel-title { |
| | | border-left: 4px solid #e6a23c; |
| | | } |
| | | |
| | | :deep(.el-date-picker) { |
| | | width: 100%; |
| | | } |
| | | .chart-container { |
| | | flex: 1; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .table-container { |
| | | flex: 1; |
| | | padding: 20px; |
| | | overflow: auto; |
| | | } |
| | | |
| | | .stats-grid { |
| | | flex: 1; |
| | | padding: 15px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .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%; |
| | | } |
| | | |
| | | .stat-value { |
| | | font-size: 24px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 5px; |
| | | } |
| | | |
| | | .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> |