From a563ea879ef5fb6897e76d2df661e465dce2ab9b Mon Sep 17 00:00:00 2001
From: huminmin <mac@MacBook-Pro.local>
Date: 星期一, 01 六月 2026 15:02:27 +0800
Subject: [PATCH] Merge branch 'dev_新疆_大罗素马铃薯new' of http://114.132.189.42:9002/r/product-inventory-management into dev_新疆_大罗素马铃薯new

---
 src/views/salesManagement/indicatorStats/index.vue |  715 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 715 insertions(+), 0 deletions(-)

diff --git a/src/views/salesManagement/indicatorStats/index.vue b/src/views/salesManagement/indicatorStats/index.vue
new file mode 100644
index 0000000..8ae15ed
--- /dev/null
+++ b/src/views/salesManagement/indicatorStats/index.vue
@@ -0,0 +1,715 @@
+<template>
+  <div class="app-container indicator-stats">
+    <!-- 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>
+          <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>
+          <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>
+          <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-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="monthrange"
+                range-separator="鑷�"
+                start-placeholder="寮�濮嬫棩鏈�" 
+                end-placeholder="缁撴潫鏃ユ湡" 
+                value-format="YYYY-MM"
+                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>
+
+      <!-- 鍥捐〃灞曠ず鍖� -->
+      <div class="chart-container" v-loading="loading">
+        <div ref="indicatorChartRef" class="chart-wrapper"></div>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onUnmounted, nextTick } from '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'
+import { customerList } from '@/api/salesManagement/salesLedger.js'
+import { ElMessage } from 'element-plus'
+
+const indicatorKpis = reactive({
+  orderCount: 0,
+  salesAmount: 0,
+  shipRate: 0
+})
+
+// 鏄惁灞曠ず閿�鍞洟闃熸槑缁嗚〃锛屾寜闇�寮�鍚�
+const showTeamPerformance = ref(false)
+const loading = ref(false)
+
+const indicatorFilter = reactive({
+  productCategory: '',
+  customerName: '',
+  dateRange: []
+})
+
+const indicatorChartRef = ref(null)
+let indicatorChart = null
+
+const productOptions = ref([])
+const customerOption = ref([])
+
+// 杞崲浜у搧鏍戞暟鎹紝灏� id 鏀逛负 value
+function convertIdToValue(data) {
+  return data.map((item) => {
+    const { id, children, ...rest } = item
+    const newItem = {
+      ...rest,
+      value: id, // 灏� id 鏀逛负 value
+    }
+    if (children && children.length > 0) {
+      newItem.children = convertIdToValue(children)
+    }
+    return newItem
+  })
+}
+
+// 鑾峰彇浜у搧鏍戞暟鎹�
+const getProductOptions = () => {
+  return productTreeList().then((res) => {
+    productOptions.value = convertIdToValue(res)
+  }).catch((error) => {
+    console.error('鑾峰彇浜у搧鏍戝け璐�:', error)
+    ElMessage.error('鑾峰彇浜у搧绫诲埆澶辫触')
+  })
+}
+
+// 鑾峰彇瀹㈡埛鍒楄〃
+const getCustomerList = () => {
+  return customerList().then((res) => {
+    customerOption.value = res || []
+  }).catch((error) => {
+    console.error('鑾峰彇瀹㈡埛鍒楄〃澶辫触:', error)
+    ElMessage.error('鑾峰彇瀹㈡埛鍒楄〃澶辫触')
+  })
+}
+
+// 鏍规嵁 id 鏌ユ壘浜у搧绫诲埆鍚嶇О
+const findNodeLabelById = (nodes, id) => {
+  if (!id) return null
+  for (let i = 0; i < nodes.length; i++) {
+    if (nodes[i].value === id) {
+      return nodes[i].label
+    }
+    if (nodes[i].children && nodes[i].children.length > 0) {
+      const found = findNodeLabelById(nodes[i].children, id)
+      if (found) return found
+    }
+  }
+  return null
+}
+
+// 鑾峰彇澶撮儴缁熻鏁版嵁
+const fetchTotalStatistics = async () => {
+  try {
+    loading.value = true
+    const params = {}
+    if (indicatorFilter.customerName) {
+      params.customerName = indicatorFilter.customerName
+    }
+    if (indicatorFilter.productCategory) {
+      // 鏍规嵁 id 鏌ユ壘浜у搧绫诲埆鍚嶇О
+      const categoryName = findNodeLabelById(productOptions.value, indicatorFilter.productCategory)
+      if (categoryName) {
+        params.productCategory = categoryName
+      }
+    }
+    if (indicatorFilter.dateRange && indicatorFilter.dateRange.length === 2) {
+      params.entryDateStart = indicatorFilter.dateRange[0]
+      params.entryDateEnd = indicatorFilter.dateRange[1]
+    }
+    const res = await getTotalStatistics(params)
+    if (res && res.data) {
+      indicatorKpis.orderCount = res.data.total || 0
+      indicatorKpis.salesAmount = res.data.contractAmountTotal || 0
+      // 鍙戣揣鐜囧鏋滄帴鍙f病鏈夎繑鍥烇紝淇濇寔鍘熷�兼垨璁句负0
+      indicatorKpis.shipRate = res.data.shipRate || 0
+    }
+  } catch (error) {
+    console.error('鑾峰彇澶撮儴缁熻澶辫触:', error)
+    ElMessage.error('鑾峰彇缁熻鏁版嵁澶辫触')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 鑾峰彇鏌辩姸鍥炬暟鎹�
+const fetchStatisticsTable = async () => {
+  try {
+    loading.value = true
+    const params = {}
+    if (indicatorFilter.customerName) {
+      params.customerName = indicatorFilter.customerName
+    }
+    if (indicatorFilter.productCategory) {
+      // 鏍规嵁 id 鏌ユ壘浜у搧绫诲埆鍚嶇О
+      const categoryName = findNodeLabelById(productOptions.value, indicatorFilter.productCategory)
+      if (categoryName) {
+        params.productCategory = categoryName
+      }
+    }
+    if (indicatorFilter.dateRange && indicatorFilter.dateRange.length === 2) {
+      params.entryDateStart = indicatorFilter.dateRange[0]
+      params.entryDateEnd = indicatorFilter.dateRange[1]
+    }
+    const res = await getStatisticsTable(params)
+    if (res && res.data) {
+      updateChart(res.data)
+    }
+  } catch (error) {
+    console.error('鑾峰彇鍥捐〃鏁版嵁澶辫触:', error)
+    ElMessage.error('鑾峰彇鍥捐〃鏁版嵁澶辫触')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 鏇存柊鍥捐〃
+const updateChart = (chartData) => {
+  if (!indicatorChartRef.value) return
+  if (indicatorChart) indicatorChart.dispose()
+  indicatorChart = echarts.init(indicatorChartRef.value)
+  
+  // 鏍规嵁鎺ュ彛杩斿洖鐨勬暟鎹粨鏋勬洿鏂板浘琛�
+  // 鎺ュ彛杩斿洖: dateList, orderCountList, salesAmountList
+  const option = {
+    title: { text: '澶氱淮搴﹂攢鍞寚鏍囪秼鍔�', left: 'center' },
+    tooltip: { trigger: 'axis' },
+    legend: { data: ['璁㈠崟鏁�', '閿�鍞'], top: 30 },
+    grid: { left: '3%', right: '8%', bottom: '3%', containLabel: true },
+    xAxis: { 
+      type: 'category', 
+      data: chartData.dateList || [] 
+    },
+    yAxis: [
+      { type: 'value', name: '閲戦', position: 'left', axisLabel: { formatter: '{value}' } },
+      { 
+        type: 'value', 
+        name: '鏁伴噺', 
+        position: 'right', 
+        minInterval: 1,
+        axisLabel: { 
+          formatter: (value) => {
+            const intValue = Math.round(value)
+            return intValue.toString()
+          }
+        }
+      }
+    ],
+    series: [
+      { 
+        name: '璁㈠崟鏁�', 
+        type: 'line', 
+        yAxisIndex: 1,
+        data: chartData.orderCountList || [], 
+        itemStyle: { color: '#409eff' } 
+      },
+      { 
+        name: '閿�鍞', 
+        type: 'bar', 
+        yAxisIndex: 0,
+        data: chartData.salesAmountList || [], 
+        itemStyle: { color: '#67c23a' } 
+      }
+    ]
+  }
+  indicatorChart.setOption(option)
+}
+
+const initIndicatorChart = () => {
+  if (!indicatorChartRef.value) return
+  if (indicatorChart) indicatorChart.dispose()
+  indicatorChart = echarts.init(indicatorChartRef.value)
+  const option = {
+    title: { text: '澶氱淮搴﹂攢鍞寚鏍囪秼鍔�', left: 'center' },
+    tooltip: { trigger: 'axis' },
+    legend: { data: ['璁㈠崟鏁�', '閿�鍞'], top: 30 },
+    grid: { left: '3%', right: '8%', bottom: '3%', containLabel: true },
+    xAxis: { type: 'category', data: [] },
+    yAxis: [
+      { type: 'value', name: '閲戦', position: 'left', axisLabel: { formatter: '{value}' } },
+      { 
+        type: 'value', 
+        name: '鏁伴噺', 
+        position: 'right', 
+        minInterval: 1,
+        axisLabel: { 
+          formatter: (value) => {
+            const intValue = Math.round(value)
+            return intValue.toString()
+          }
+        }
+      }
+    ],
+    series: [
+      { name: '璁㈠崟鏁�', type: 'line', yAxisIndex: 1, data: [], itemStyle: { color: '#409eff' } },
+      { name: '閿�鍞', type: 'bar', yAxisIndex: 0, data: [], itemStyle: { color: '#67c23a' } }
+    ]
+  }
+  indicatorChart.setOption(option)
+}
+
+const applyIndicatorFilter = async () => {
+  // 绛涢�夋潯浠跺彧褰卞搷鍥捐〃鏁版嵁锛屼笉褰卞搷KPI姹囨��
+  await fetchStatisticsTable()
+}
+
+const resetIndicatorFilter = () => {
+  indicatorFilter.productCategory = ''
+  indicatorFilter.customerName = ''
+  indicatorFilter.dateRange = []
+  applyIndicatorFilter()
+}
+
+// 绐楀彛澶у皬鍙樺寲鏃惰皟鏁村浘琛ㄥぇ灏�
+const handleResize = () => {
+  if (indicatorChart) {
+    indicatorChart.resize()
+  }
+}
+
+onMounted(() => {
+  nextTick(() => {
+    initIndicatorChart()
+    getProductOptions()
+    getCustomerList()
+    fetchTotalStatistics()
+    fetchStatisticsTable()
+  })
+  // 鐩戝惉绐楀彛澶у皬鍙樺寲
+  window.addEventListener('resize', handleResize)
+})
+
+onUnmounted(() => {
+  // 绉婚櫎绐楀彛澶у皬鍙樺寲鐩戝惉鍣�
+  window.removeEventListener('resize', handleResize)
+  // 閿�姣佸浘琛ㄥ疄渚�
+  if (indicatorChart) {
+    indicatorChart.dispose()
+    indicatorChart = null
+  }
+})
+</script>
+
+<style scoped lang="scss">
+.indicator-stats {
+  padding: 20px;
+  min-height: calc(100vh - 84px);
+}
+
+.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;
+  }
+}
+
+.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>
+
+

--
Gitblit v1.9.3