| | |
| | | <template> |
| | | <div class="app-container indicator-stats"> |
| | | <el-card class="box-card"> |
| | | <!-- KPI 汇总 --> |
| | | <el-row :gutter="20" class="stats-row"> |
| | | <el-col :span="8"> |
| | | <div class="stat-card"> |
| | | <div class="stat-icon" style="background: #ecf5ff;"> |
| | | <el-icon :size="30" color="#409eff"><Document /></el-icon> |
| | | </div> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">{{ indicatorKpis.orderCount.toLocaleString() }}</div> |
| | | <div class="stat-label">订单数量</div> |
| | | <!-- KPI 汇总 --> |
| | | <el-row :gutter="20" class="stats-row"> |
| | | <el-col :xs="24" :sm="12" :md="8"> |
| | | <div class="stat-card stat-card-blue"> |
| | | <div class="stat-icon-wrapper"> |
| | | <div class="stat-icon"> |
| | | <el-icon :size="32"><Document /></el-icon> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <div class="stat-card"> |
| | | <div class="stat-icon" style="background: #f0f9ff;"> |
| | | <el-icon :size="30" color="#67c23a"><Tickets /></el-icon> |
| | | </div> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">¥{{ indicatorKpis.salesAmount.toLocaleString() }}</div> |
| | | <div class="stat-label">销售额</div> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">{{ indicatorKpis.orderCount.toLocaleString() }}</div> |
| | | <div class="stat-label">订单数量</div> |
| | | </div> |
| | | <div class="stat-bg-decoration"></div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :xs="24" :sm="12" :md="8"> |
| | | <div class="stat-card stat-card-green"> |
| | | <div class="stat-icon-wrapper"> |
| | | <div class="stat-icon"> |
| | | <el-icon :size="32"><Tickets /></el-icon> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <div class="stat-card"> |
| | | <div class="stat-icon" style="background: #fef0f0;"> |
| | | <el-icon :size="30" color="#e6a23c"><Van /></el-icon> |
| | | </div> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">{{ indicatorKpis.shipRate }}%</div> |
| | | <div class="stat-label">发货率</div> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">¥{{ indicatorKpis.salesAmount.toLocaleString() }}</div> |
| | | <div class="stat-label">销售额</div> |
| | | </div> |
| | | <div class="stat-bg-decoration"></div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :xs="24" :sm="12" :md="8"> |
| | | <div class="stat-card stat-card-orange"> |
| | | <div class="stat-icon-wrapper"> |
| | | <div class="stat-icon"> |
| | | <el-icon :size="32"><Van /></el-icon> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">{{ indicatorKpis.shipRate }}%</div> |
| | | <div class="stat-label">发货率</div> |
| | | </div> |
| | | <div class="stat-bg-decoration"></div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 维度筛选 --> |
| | | <el-row :gutter="20" class="search-row"> |
| | | <el-col :span="6"> |
| | | <el-tree-select v-model="indicatorFilter.productCategory" placeholder="产品类别" clearable check-strictly |
| | | :data="productOptions" :render-after-expand="false" style="width: 100%" /> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-select v-model="indicatorFilter.customerName" placeholder="客户" clearable filterable> |
| | | <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.customerName" /> |
| | | </el-select> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-date-picker v-model="indicatorFilter.dateRange" type="daterange" range-separator="至" |
| | | start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" style="width: 100%" /> |
| | | </el-col> |
| | | <el-col :span="6" style="text-align: right;"> |
| | | <el-button type="primary" @click="applyIndicatorFilter">查询</el-button> |
| | | <el-button @click="resetIndicatorFilter">重置</el-button> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 图表区 --> |
| | | <div class="chart-container"> |
| | | <div ref="indicatorChartRef" class="chart-wrapper"></div> |
| | | <!-- 图表区(包含筛选条件) --> |
| | | <el-card class="chart-card" shadow="hover"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <div class="header-left"> |
| | | <span class="card-title">销售趋势分析</span> |
| | | <span class="card-subtitle">筛选条件仅影响下方图表数据</span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <!-- 图表筛选条件 --> |
| | | <div class="chart-filter-section"> |
| | | <el-row :gutter="16" class="search-row"> |
| | | <el-col :xs="24" :sm="12" :md="6"> |
| | | <div class="filter-item"> |
| | | <label class="filter-label">产品类别</label> |
| | | <el-tree-select |
| | | v-model="indicatorFilter.productCategory" |
| | | placeholder="请选择产品类别" |
| | | clearable |
| | | check-strictly |
| | | :data="productOptions" |
| | | :render-after-expand="false" |
| | | style="width: 100%" |
| | | /> |
| | | </div> |
| | | </el-col> |
| | | <el-col :xs="24" :sm="12" :md="6"> |
| | | <div class="filter-item"> |
| | | <label class="filter-label">客户</label> |
| | | <el-select |
| | | v-model="indicatorFilter.customerName" |
| | | placeholder="请选择客户" |
| | | clearable |
| | | filterable |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="item in customerOption" |
| | | :key="item.id" |
| | | :label="item.customerName" |
| | | :value="item.customerName" |
| | | /> |
| | | </el-select> |
| | | </div> |
| | | </el-col> |
| | | <el-col :xs="24" :sm="12" :md="6"> |
| | | <div class="filter-item"> |
| | | <label class="filter-label">日期范围</label> |
| | | <el-date-picker |
| | | v-model="indicatorFilter.dateRange" |
| | | type="daterange" |
| | | range-separator="至" |
| | | start-placeholder="开始日期" |
| | | end-placeholder="结束日期" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 100%" |
| | | /> |
| | | </div> |
| | | </el-col> |
| | | <el-col :xs="24" :sm="12" :md="6"> |
| | | <div class="filter-item filter-buttons"> |
| | | <el-button type="primary" :loading="loading" @click="applyIndicatorFilter"> |
| | | <el-icon><Search /></el-icon> |
| | | 查询图表 |
| | | </el-button> |
| | | <el-button @click="resetIndicatorFilter"> |
| | | <el-icon><Refresh /></el-icon> |
| | | 重置 |
| | | </el-button> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | |
| | | <!-- 业绩统计(团队维度,无个人姓名) --> |
| | | <el-table v-if="showTeamPerformance" :data="teamPerformanceList" border stripe style="margin-top: 20px;"> |
| | | <el-table-column prop="team" label="销售团队"/> |
| | | <el-table-column prop="orderCount" label="订单数"/> |
| | | <el-table-column prop="salesAmount" label="销售额"> |
| | | <!-- 图表展示区 --> |
| | | <div class="chart-container" v-loading="loading"> |
| | | <div ref="indicatorChartRef" class="chart-wrapper"></div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- 业绩统计(团队维度,无个人姓名) --> |
| | | <el-card v-if="showTeamPerformance" class="table-card" shadow="hover"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span class="card-title">团队业绩统计</span> |
| | | </div> |
| | | </template> |
| | | <el-table |
| | | :data="teamPerformanceList" |
| | | border |
| | | stripe |
| | | style="width: 100%" |
| | | :header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 'bold' }" |
| | | > |
| | | <el-table-column prop="team" label="销售团队" min-width="120"/> |
| | | <el-table-column prop="orderCount" label="订单数" align="right" min-width="100"/> |
| | | <el-table-column prop="salesAmount" label="销售额" align="right" min-width="140"> |
| | | <template #default="scope">¥{{ scope.row.salesAmount.toLocaleString() }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="shipRate" label="发货率"> |
| | | <template #default="scope">{{ scope.row.shipRate }}</template> |
| | | <el-table-column prop="shipRate" label="发货率" align="right" min-width="100"> |
| | | <template #default="scope">{{ scope.row.shipRate }}%</template> |
| | | </el-table-column> |
| | | <el-table-column prop="attainment" label="目标达成率"> |
| | | <el-table-column prop="attainment" label="目标达成率" align="center" min-width="120"> |
| | | <template #default="scope"> |
| | | <el-tag :type="scope.row.attainment >= 100 ? 'success' : scope.row.attainment >= 80 ? 'warning' : 'danger'"> |
| | | <el-tag |
| | | :type="scope.row.attainment >= 100 ? 'success' : scope.row.attainment >= 80 ? 'warning' : 'danger'" |
| | | effect="dark" |
| | | > |
| | | {{ scope.row.attainment }}% |
| | | </el-tag> |
| | | </template> |
| | |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue' |
| | | import { Document, Van, Tickets } from '@element-plus/icons-vue' |
| | | import { Document, Van, Tickets, Search, Refresh } from '@element-plus/icons-vue' |
| | | import * as echarts from 'echarts' |
| | | import { getTotalStatistics, getStatisticsTable } from '@/api/salesManagement/indicatorStats' |
| | | import { productTreeList } from '@/api/basicData/product.js' |
| | |
| | | } |
| | | |
| | | const applyIndicatorFilter = async () => { |
| | | await Promise.all([ |
| | | fetchTotalStatistics(), |
| | | fetchStatisticsTable() |
| | | ]) |
| | | // 筛选条件只影响图表数据,不影响KPI汇总 |
| | | await fetchStatisticsTable() |
| | | } |
| | | |
| | | const resetIndicatorFilter = () => { |
| | |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | <style scoped lang="scss"> |
| | | .indicator-stats { |
| | | padding: 0; |
| | | padding: 20px; |
| | | background: #f5f7fa; |
| | | min-height: calc(100vh - 84px); |
| | | } |
| | | .box-card { border: none; box-shadow: none; } |
| | | .search-row { margin-bottom: 20px; } |
| | | .stats-row { margin-bottom: 24px; } |
| | | .stat-card { display: flex; align-items: center; padding: 20px; background: #fff; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } |
| | | .stat-icon { width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; border-radius: 8px; margin-right: 16px; } |
| | | .stat-content { flex: 1; } |
| | | .stat-value { font-size: 28px; font-weight: bold; color: #303133; margin-bottom: 4px; } |
| | | .stat-label { font-size: 14px; color: #909399; } |
| | | .chart-container { |
| | | margin: 20px 0; |
| | | padding: 20px; |
| | | background: #fff; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | |
| | | .page-header { |
| | | margin-bottom: 24px; |
| | | padding: 20px 0; |
| | | |
| | | .page-title { |
| | | font-size: 24px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin: 0 0 8px 0; |
| | | } |
| | | |
| | | .page-desc { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | margin: 0; |
| | | } |
| | | } |
| | | |
| | | .stats-row { |
| | | margin-bottom: 24px; |
| | | } |
| | | |
| | | .stat-card { |
| | | position: relative; |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 24px; |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08); |
| | | transition: all 0.3s ease; |
| | | overflow: hidden; |
| | | |
| | | &:hover { |
| | | transform: translateY(-4px); |
| | | box-shadow: 0 8px 24px 0 rgba(0, 0, 0, 0.12); |
| | | } |
| | | |
| | | .stat-icon-wrapper { |
| | | margin-right: 20px; |
| | | |
| | | .stat-icon { |
| | | width: 64px; |
| | | height: 64px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 12px; |
| | | transition: all 0.3s ease; |
| | | } |
| | | } |
| | | |
| | | .stat-content { |
| | | flex: 1; |
| | | z-index: 1; |
| | | |
| | | .stat-value { |
| | | font-size: 32px; |
| | | font-weight: 700; |
| | | color: #303133; |
| | | margin-bottom: 8px; |
| | | line-height: 1.2; |
| | | } |
| | | |
| | | .stat-label { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | |
| | | .stat-bg-decoration { |
| | | position: absolute; |
| | | right: -20px; |
| | | top: -20px; |
| | | width: 120px; |
| | | height: 120px; |
| | | border-radius: 50%; |
| | | opacity: 0.1; |
| | | z-index: 0; |
| | | } |
| | | |
| | | &.stat-card-blue { |
| | | .stat-icon { |
| | | background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%); |
| | | color: #fff; |
| | | } |
| | | |
| | | .stat-bg-decoration { |
| | | background: #409eff; |
| | | } |
| | | } |
| | | |
| | | &.stat-card-green { |
| | | .stat-icon { |
| | | background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%); |
| | | color: #fff; |
| | | } |
| | | |
| | | .stat-bg-decoration { |
| | | background: #67c23a; |
| | | } |
| | | } |
| | | |
| | | &.stat-card-orange { |
| | | .stat-icon { |
| | | background: linear-gradient(135deg, #e6a23c 0%, #ebb563 100%); |
| | | color: #fff; |
| | | } |
| | | |
| | | .stat-bg-decoration { |
| | | background: #e6a23c; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .chart-card, |
| | | .table-card { |
| | | margin-bottom: 20px; |
| | | border-radius: 12px; |
| | | border: none; |
| | | |
| | | :deep(.el-card__header) { |
| | | padding: 18px 20px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%); |
| | | } |
| | | |
| | | :deep(.el-card__body) { |
| | | padding: 0; |
| | | } |
| | | } |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | |
| | | .header-left { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .card-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .card-subtitle { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | font-weight: normal; |
| | | } |
| | | } |
| | | |
| | | .chart-filter-section { |
| | | padding: 20px; |
| | | background: #fafbfc; |
| | | border-bottom: 1px solid #ebeef5; |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .search-row { |
| | | .filter-item { |
| | | margin-bottom: 0; |
| | | |
| | | .filter-label { |
| | | display: block; |
| | | font-size: 13px; |
| | | color: #606266; |
| | | margin-bottom: 8px; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | &.filter-buttons { |
| | | display: flex; |
| | | align-items: flex-end; |
| | | gap: 10px; |
| | | padding-top: 28px; |
| | | |
| | | .el-button { |
| | | flex: 1; |
| | | font-size: 14px; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .chart-container { |
| | | width: 100%; |
| | | overflow: hidden; |
| | | position: relative; |
| | | padding: 20px; |
| | | background: #fff; |
| | | |
| | | .chart-wrapper { |
| | | width: 100%; |
| | | height: 420px; |
| | | min-width: 0; |
| | | } |
| | | } |
| | | .chart-wrapper { |
| | | width: 100%; |
| | | height: 360px; |
| | | min-width: 0; |
| | | |
| | | .table-card { |
| | | :deep(.el-table) { |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | :deep(.el-table__header-wrapper) { |
| | | .el-table__header { |
| | | th { |
| | | background: #f5f7fa; |
| | | color: #606266; |
| | | font-weight: 600; |
| | | } |
| | | } |
| | | } |
| | | |
| | | :deep(.el-table__body-wrapper) { |
| | | .el-table__body { |
| | | tr:hover { |
| | | background-color: #f5f7fa; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 响应式设计 |
| | | @media (max-width: 768px) { |
| | | .indicator-stats { |
| | | padding: 12px; |
| | | } |
| | | |
| | | .stat-card { |
| | | padding: 20px; |
| | | |
| | | .stat-content .stat-value { |
| | | font-size: 24px; |
| | | } |
| | | |
| | | .stat-icon-wrapper .stat-icon { |
| | | width: 56px; |
| | | height: 56px; |
| | | } |
| | | } |
| | | |
| | | .chart-filter-section { |
| | | padding: 16px; |
| | | } |
| | | |
| | | .search-row { |
| | | .filter-item.filter-buttons { |
| | | padding-top: 0; |
| | | margin-top: 12px; |
| | | } |
| | | } |
| | | |
| | | .chart-container { |
| | | padding: 16px; |
| | | |
| | | .chart-wrapper { |
| | | height: 320px; |
| | | } |
| | | } |
| | | |
| | | .card-header { |
| | | .header-left { |
| | | .card-title { |
| | | font-size: 15px; |
| | | } |
| | | |
| | | .card-subtitle { |
| | | font-size: 11px; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 576px) { |
| | | .page-header { |
| | | .page-title { |
| | | font-size: 20px; |
| | | } |
| | | |
| | | .page-desc { |
| | | font-size: 12px; |
| | | } |
| | | } |
| | | |
| | | .stat-card { |
| | | flex-direction: column; |
| | | text-align: center; |
| | | |
| | | .stat-icon-wrapper { |
| | | margin-right: 0; |
| | | margin-bottom: 12px; |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | | |