zouyu
6 天以前 1c0863efe062af3ebcdecb8c10568d779f5c8295
src/views/procurementManagement/procurementReport/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,380 @@
<template>
  <div class="app-container">
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <el-form :model="searchForm" :inline="true" class="search-form">
        <el-form-item label="时间范围:">
          <el-date-picker
            v-model="searchForm.dateRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            format="YYYY-MM-DD"
            value-format="YYYY-MM-DD"
            style="width: 240px"
          />
        </el-form-item>
        <el-form-item label="产品大类:">
          <el-tree-select
            v-model="searchForm.productCategory"
            placeholder="请选择商品类别"
            clearable
            check-strictly
            :data="productOptions"
            :render-after-expand="false"
            style="width: 200px"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch" :loading="loading">
            æŸ¥è¯¢
          </el-button>
          <el-button @click="resetSearch">
            é‡ç½®
          </el-button>
          <el-button type="info" @click="exportReport">
            <el-icon><Download /></el-icon>
            å¯¼å‡º
          </el-button>
        </el-form-item>
      </el-form>
    <!-- æŠ¥è¡¨å†…容 -->
    <el-card class="report-content" shadow="never">
      <!-- é‡‡è´­ä¸šåŠ¡æ±‡æ€»è¡¨ -->
      <div class="report-section">
        <div class="section-header">
          <h3>采购业务汇总表</h3>
          <div class="summary-stats">
            <div class="stat-item">
              <span class="stat-label">采购总额:</span>
              <span class="stat-value">Â¥{{ businessSummaryStats.totalAmount.toLocaleString() }}</span>
            </div>
            <div class="stat-item">
              <span class="stat-label">商品种类:</span>
              <span class="stat-value">{{ businessSummaryStats.productTypes }}</span>
            </div>
          </div>
        </div>
        <PIMTable
          :table-data="businessSummaryData"
          :column="tableColumns"
          :table-loading="loading"
          :is-selection="false"
          :border="true"
          :is-show-pagination="true"
          :page="page"
          @pagination="handlePagination"
        />
      </div>
    </el-card>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Download } from '@element-plus/icons-vue'
import PIMTable from '@/components/PIMTable/PIMTable.vue'
import { procurementBusinessSummaryListPage } from '@/api/procurementManagement/procurementReport'
import { productTreeList } from '@/api/basicData/product'
// å“åº”式数据
const loading = ref(false)
// æœç´¢è¡¨å•
const searchForm = reactive({
  dateRange: [],
  productCategory: ''
})
// äº§å“ç±»åˆ«æ ‘选项
const productOptions = ref([])
// ç»Ÿè®¡æ•°æ®
const businessSummaryStats = ref({
  totalAmount: 0,
  productTypes: 0
})
// è¡¨æ ¼åˆ—配置(根据后端字段定义)
const tableColumns = ref([
  {
    label: '产品大类',
    prop: 'productCategory',
    width: 150,
  },
  {
    label: '规格型号',
    prop: 'specificationModel',
    width: 180
  },
  {
    label: '采购数量',
    prop: 'purchaseNum',
    width: 120,
    formatData: (val) => {
      return val ? parseFloat(val).toLocaleString() : '0'
    }
  },
  {
    label: '采购金额',
    prop: 'purchaseAmount',
    width: 140,
    formatData: (val) => {
      return val ? `Â¥${parseFloat(val).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : 'Â¥0.00'
    }
  },
  {
    label: '采购次数',
    prop: 'purchaseTimes',
    width: 100
  },
  {
    label: '平均单价',
    prop: 'averagePrice',
    width: 120,
    formatData: (val) => {
      return val ? `Â¥${parseFloat(val).toFixed(2)}` : 'Â¥0.00'
    }
  },
  {
    label: '供应商名称',
    prop: 'supplierName',
    width: 200
  },
  {
    label: '录入日期',
    prop: 'entryDate',
    width: 120
  }
])
// é‡‡è´­ä¸šåŠ¡æ±‡æ€»è¡¨æ•°æ®
const businessSummaryData = ref([])
// åˆ†é¡µå‚数(后端返回:total/size/current/pages)
const page = reactive({
  total: 0,
  current: 1,
  size: 50,
})
// è½¬æ¢äº§å“æ ‘数据,将 id æ”¹ä¸º value
function convertIdToValue(data) {
  return data.map((item) => {
    const { id, children, ...rest } = item
    const newItem = {
      ...rest,
      value: id,
    }
    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('获取产品类别失败')
  })
}
// æ ¹æ® 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 handleSearch = async () => {
  try {
    loading.value = true
    const params = {}
    // æ—¶é—´èŒƒå›´
    if (searchForm.dateRange && searchForm.dateRange.length === 2) {
      params.entryDateStart = searchForm.dateRange[0]
      params.entryDateEnd = searchForm.dateRange[1]
    }
    // äº§å“ç±»åˆ«
    if (searchForm.productCategory) {
      const categoryName = findNodeLabelById(productOptions.value, searchForm.productCategory)
      if (categoryName) {
        params.productCategory = categoryName
      }
    }
    // åˆ†é¡µå‚æ•°
    params.current = page.current
    params.size = page.size
    const res = await procurementBusinessSummaryListPage(params)
    if (res && res.data) {
      // å…¼å®¹åŽç«¯å¯èƒ½ç›´æŽ¥è¿”回数组/或返回分页对象
      businessSummaryData.value = Array.isArray(res.data) ? res.data : (res.data.records || [])
      if (!Array.isArray(res.data)) {
        page.total = Number(res.data.total ?? 0)
        page.current = Number(res.data.current ?? page.current)
        page.size = Number(res.data.size ?? page.size)
      }
      // è®¡ç®—统计数据
      if (businessSummaryData.value.length > 0) {
        businessSummaryStats.value.totalAmount = businessSummaryData.value.reduce((sum, item) => {
          return sum + (parseFloat(item.purchaseAmount) || 0)
        }, 0)
        businessSummaryStats.value.productTypes = new Set(businessSummaryData.value.map(item => item.productCategory)).size
      } else {
        businessSummaryStats.value = {
          totalAmount: 0,
          productTypes: 0
        }
      }
    }
  } catch (error) {
    console.error('查询失败:', error)
  } finally {
    loading.value = false
  }
}
// ç¿»é¡µ/切换每页条数
const handlePagination = ({ page: current, limit }) => {
  page.current = current
  page.size = limit
  handleSearch()
}
const resetSearch = () => {
  Object.assign(searchForm, {
    dateRange: [],
    productCategory: ''
  })
  page.current = 1
  handleSearch()
}
const exportReport = () => {
  ElMessage.success('导出功能开发中...')
}
onMounted(() => {
  // åˆå§‹åŒ–产品类别树
  getProductOptions()
  // è®¾ç½®é»˜è®¤æ—¶é—´èŒƒå›´ä¸ºæœ€è¿‘30天
  const endDate = new Date()
  const startDate = new Date()
  startDate.setDate(startDate.getDate() - 30)
  searchForm.dateRange = [
    startDate.toISOString().split('T')[0],
    endDate.toISOString().split('T')[0]
  ]
  // åˆå§‹åŠ è½½æ•°æ®
  handleSearch()
})
</script>
<style scoped>
.app-container {
  padding: 20px;
  background-color: #f5f7fa;
  min-height: 100vh;
}
.page-header {
  text-align: center;
  margin-bottom: 20px;
  padding: 20px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-radius: 10px;
  color: white;
}
.page-header h2 {
  margin: 0 0 10px 0;
  font-size: 28px;
  font-weight: 600;
}
.page-header p {
  margin: 0;
  font-size: 16px;
  opacity: 0.9;
}
.report-content {
  border-radius: 8px;
}
.report-section {
  min-height: 400px;
}
.section-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
  padding-bottom: 15px;
  border-bottom: 2px solid #e4e7ed;
}
.section-header h3 {
  margin: 0;
  font-size: 20px;
  font-weight: 600;
  color: #303133;
}
.summary-stats {
  display: flex;
  gap: 30px;
}
.stat-item {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.stat-label {
  font-size: 14px;
  color: #606266;
  margin-bottom: 5px;
}
.stat-value {
  font-size: 18px;
  font-weight: 600;
  color: #409EFF;
}
.delay-text {
  color: #F56C6C;
  font-weight: 600;
}
</style>