spring
11 小时以前 fd1ab35c60963b4619be680fb7671c85c6ed0dad
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-management into dev_New
已修改13个文件
812 ■■■■ 文件已修改
src/api/procurementManagement/procurementLedger.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/supplierManage/components/BlacklistTab.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/supplierManage/components/HomeTab.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/supplierManage/index.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Import.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Qualified.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockReport/index.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/deliveryLedger/index.vue 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/indicatorStats/index.vue 475 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 141 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/procurementManagement/procurementLedger.js
@@ -115,3 +115,11 @@
        data: id,
    });
}
// 查询采购详情
export function getPurchaseByCode(id) {
    return request({
        url: "/purchase/ledger/getPurchaseByCode",
        method: "get",
        params: id,
    });
}
src/views/basicData/supplierManage/components/BlacklistTab.vue
@@ -499,7 +499,7 @@
    type: "warning",
  })
      .then(() => {
        proxy.download("/system/supplier/export", {}, "供应商档案.xlsx");
        proxy.download("/system/supplier/export", { isWhite: 1 }, "供应商档案.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
@@ -559,6 +559,10 @@
onMounted(() => {
  getList();
});
defineExpose({
  getList,
});
</script>
src/views/basicData/supplierManage/components/HomeTab.vue
@@ -505,7 +505,7 @@
    type: "warning",
  })
      .then(() => {
        proxy.download("/system/supplier/export", {}, "供应商档案.xlsx");
        proxy.download("/system/supplier/export", { isWhite: 0 }, "供应商档案.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
@@ -565,5 +565,9 @@
onMounted(() => {
  getList();
});
defineExpose({
  getList,
});
</script>
src/views/basicData/supplierManage/index.vue
@@ -1,12 +1,12 @@
<!-- 在你的主页面中 -->
<template>
  <div class="app-container">
    <el-tabs v-model="activeTab" type="card">
    <el-tabs v-model="activeTab" @tab-change="handleTabChange">
      <el-tab-pane label="正常供应商" name="home">
        <HomeTab />
        <HomeTab ref="homeTab" />
      </el-tab-pane>
      <el-tab-pane label="黑名单" name="blacklist">
        <BlacklistTab />
        <BlacklistTab ref="blacklistTab" />
      </el-tab-pane>
    </el-tabs>
  </div>
@@ -27,21 +27,17 @@
      activeTab: 'home'
    }
  },
  watch: {
    activeTab(newVal) {
      if (newVal === 'home') {
        this.$refs.homeTab && this.$refs.homeTab.getList()
      } else if (newVal === 'blacklist') {
        this.$refs.blacklistTab && this.$refs.blacklistTab.getList()
  methods: {
    handleTabChange(tabName) {
      this.activeTab = tabName
      this.$nextTick(() => {
        if (tabName === 'home') {
          this.$refs.homeTab && this.$refs.homeTab.getList && this.$refs.homeTab.getList()
        } else if (tabName === 'blacklist') {
          this.$refs.blacklistTab && this.$refs.blacklistTab.getList && this.$refs.blacklistTab.getList()
      }
    }
      })
    },
  }
}
</script>
<style>
.main-container :deep(.el-tabs__item.is-active) {
  color: #1883f6 !important;
  border-bottom: 2px solid #409EFF;
}
</style>
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue
@@ -2,7 +2,7 @@
  <div>
    <el-dialog
      v-model="dialogFormVisible"
      :title="operationType === 'add' ? '新增审批流程' : '编辑审批流程'"
      :title="operationType === 'approval' ? '审批' : '详情'"
      width="700px"
      @close="closeDia"
    >
@@ -32,9 +32,9 @@
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row v-if="!isQuotationApproval">
                <el-row v-if="!isQuotationApproval && !isPurchaseApproval">
                    <el-col :span="24">
                        <el-form-item label="审批事由:" prop="approveReason">
                        <el-form-item :label="props.approveType == 5 ? '采购合同号:' : '审批事由:'" prop="approveReason">
                            <el-input v-model="form.approveReason" placeholder="请输入" clearable type="textarea" disabled/>
                        </el-form-item>
                    </el-col>
@@ -74,7 +74,7 @@
                </el-row>
            </el-form>
      <!-- 报价审批:展示报价详情(复用销售报价“查看详情对话框”内容结构) -->
      <!-- 报价审批:展示报价详情(复用销售报价"查看详情对话框"内容结构) -->
      <div v-if="isQuotationApproval" style="margin: 10px 0 18px;">
        <el-divider content-position="left">报价详情</el-divider>
        <el-skeleton :loading="quotationLoading" animated>
@@ -115,6 +115,53 @@
              <div v-if="currentQuotation.remark" style="margin-top: 20px;">
                <h4>备注</h4>
                <p>{{ currentQuotation.remark }}</p>
              </div>
            </template>
          </template>
        </el-skeleton>
      </div>
      <!-- 采购审批:展示采购详情 -->
      <div v-if="isPurchaseApproval" style="margin: 10px 0 18px;">
        <el-divider content-position="left">采购详情</el-divider>
        <el-skeleton :loading="purchaseLoading" animated>
          <template #template>
            <el-skeleton-item variant="h3" style="width: 30%" />
            <el-skeleton-item variant="text" style="width: 100%" />
            <el-skeleton-item variant="text" style="width: 100%" />
          </template>
          <template #default>
            <el-empty v-if="!currentPurchase || !currentPurchase.purchaseContractNumber" description="未查询到对应采购详情" />
            <template v-else>
              <el-descriptions :column="2" border>
                <el-descriptions-item label="采购合同号">{{ currentPurchase.purchaseContractNumber }}</el-descriptions-item>
                <el-descriptions-item label="供应商名称">{{ currentPurchase.supplierName }}</el-descriptions-item>
                <el-descriptions-item label="项目名称">{{ currentPurchase.projectName }}</el-descriptions-item>
                <el-descriptions-item label="销售合同号">{{ currentPurchase.salesContractNo }}</el-descriptions-item>
                <el-descriptions-item label="签订日期">{{ currentPurchase.executionDate }}</el-descriptions-item>
                <el-descriptions-item label="录入日期">{{ currentPurchase.entryDate }}</el-descriptions-item>
                <el-descriptions-item label="付款方式">{{ currentPurchase.paymentMethod }}</el-descriptions-item>
                <el-descriptions-item label="合同金额" :span="2">
                  <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">
                    ¥{{ Number(currentPurchase.contractAmount ?? 0).toFixed(2) }}
                  </span>
                </el-descriptions-item>
              </el-descriptions>
              <div style="margin-top: 20px;">
                <h4>产品明细</h4>
                <el-table :data="currentPurchase.productData || []" border style="width: 100%">
                  <el-table-column prop="productCategory" label="产品名称" />
                  <el-table-column prop="specificationModel" label="规格型号" />
                  <el-table-column prop="unit" label="单位" />
                  <el-table-column prop="quantity" label="数量" />
                  <el-table-column prop="taxInclusiveUnitPrice" label="含税单价">
                    <template #default="scope">¥{{ Number(scope.row.taxInclusiveUnitPrice ?? 0).toFixed(2) }}</template>
                  </el-table-column>
                  <el-table-column prop="taxInclusiveTotalPrice" label="含税总价">
                    <template #default="scope">¥{{ Number(scope.row.taxInclusiveTotalPrice ?? 0).toFixed(2) }}</template>
                  </el-table-column>
                </el-table>
              </div>
            </template>
          </template>
@@ -188,6 +235,7 @@
import {userListNoPageByTenantId} from "@/api/system/user.js";
import { WarningFilled, Edit, Check, MoreFilled } from '@element-plus/icons-vue'
import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js";
const emit = defineEmits(['close'])
const { proxy } = getCurrentInstance()
@@ -207,7 +255,10 @@
const userList = ref([])
const quotationLoading = ref(false)
const currentQuotation = ref({})
const purchaseLoading = ref(false)
const currentPurchase = ref({})
const isQuotationApproval = computed(() => Number(props.approveType) === 6)
const isPurchaseApproval = computed(() => Number(props.approveType) === 5)
const data = reactive({
    form: {
@@ -247,6 +298,7 @@
  operationType.value = type;
  dialogFormVisible.value = true;
  currentQuotation.value = {}
  currentPurchase.value = {}
    userListNoPageByTenantId().then((res) => {
        userList.value = res.data;
    });
@@ -277,7 +329,7 @@
        });
    });
  // 报价审批:用审批事由字段承载的“报价单号”去查报价列表
  // 报价审批:用审批事由字段承载的"报价单号"去查报价列表
  if (isQuotationApproval.value) {
    const quotationNo = row?.approveReason;
    if (quotationNo) {
@@ -287,6 +339,22 @@
        currentQuotation.value = records[0] || {}
      }).finally(() => {
        quotationLoading.value = false
      })
    }
  }
  // 采购审批:用审批事由字段承载的"采购合同号"去查采购详情
  if (isPurchaseApproval.value) {
    const purchaseContractNumber = row?.approveReason;
    if (purchaseContractNumber) {
      purchaseLoading.value = true
      getPurchaseByCode({ purchaseContractNumber }).then((res) => {
        currentPurchase.value = res
      }).catch((err) => {
        console.error('查询采购详情失败:', err)
        proxy.$modal.msgError('查询采购详情失败')
      }).finally(() => {
        purchaseLoading.value = false
      })
    }
  }
@@ -341,6 +409,8 @@
  dialogFormVisible.value = false;
  quotationLoading.value = false
  currentQuotation.value = {}
  purchaseLoading.value = false
  currentPurchase.value = {}
  emit('close')
};
defineExpose({
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue
@@ -35,7 +35,7 @@
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item :label="props.approveType == 5 ? '采购说明:' : '审批事由:'" prop="approveReason">
            <el-form-item :label="props.approveType == 5 ? '采购合同号:' : '审批事由:'" prop="approveReason">
              <el-input v-model="form.approveReason" placeholder="请输入" clearable type="textarea" />
            </el-form-item>
          </el-col>
src/views/collaborativeApproval/approvalProcess/index.vue
@@ -113,6 +113,7 @@
  const isLeaveType = currentApproveType.value === 2; // 请假管理
  const isReimburseType = currentApproveType.value === 4; // 报销管理
  const isQuotationType = currentApproveType.value === 6; // 报价审批
  const isPurchaseType = currentApproveType.value === 5; // 采购审批
  
  // 基础列配置
  const baseColumns = [
@@ -159,7 +160,7 @@
      width: 220
    },
    {
      label: isQuotationType ? "报价单号" : "审批事由",
      label: isQuotationType ? "报价单号" : isPurchaseType ? "采购合同号" : "审批事由",
      prop: "approveReason",
      width: 200
    },
src/views/inventoryManagement/stockManagement/Import.vue
@@ -8,6 +8,7 @@
      :disabled="upload.isUploading"
      :showTip="true"
      @success="handleFileSuccess"
      :downloadTemplate="downloadTemplate"
    />
    <template #footer>
      <div class="dialog-footer">
@@ -19,7 +20,7 @@
</template>
<script setup>
import {computed, reactive} from "vue";
import {computed, getCurrentInstance, reactive} from "vue";
import { getToken } from "@/utils/auth.js";
import { FileUpload } from "@/components/Upload";
import { ElMessage } from "element-plus";
@@ -27,6 +28,8 @@
defineOptions({
  name: "导入库存",
});
const { proxy } = getCurrentInstance()
const props = defineProps({
  visible: {
@@ -80,6 +83,10 @@
  }
};
const downloadTemplate = () => {
  proxy.download("/stockInventory/downloadStockInventory", {}, "库存导入模板.xlsx");
}
const closeModal = () => {
  isShow.value = false;
};
src/views/inventoryManagement/stockManagement/Qualified.vue
@@ -11,7 +11,6 @@
      </div>
      <div>
         <el-button type="primary" @click="isShowNewModal = true">新增库存</el-button>
        <el-button @click="importTemplate">下载导入模板</el-button>
        <el-button type="info" plain icon="Upload" @click="isShowImportModal = true">
          导入库存
        </el-button>
@@ -161,10 +160,6 @@
  }).catch(() => {
    proxy.$modal.msg("已取消")
  })
}
const importTemplate =() =>{
  proxy.download("/stockInventory/downloadStockInventory", {}, "库存导入模板.xlsx");
}
onMounted(() => {
src/views/inventoryManagement/stockReport/index.vue
@@ -166,14 +166,14 @@
             prop="createTime"
             width="200"
             show-overflow-tooltip
             v-if="!searchForm.reportType === 'inout'"
              v-if="searchForm.reportType !== 'inout'"
           />
           <el-table-column
             label="入库批次"
             prop="inboundBatches"
             width="240"
             show-overflow-tooltip
             v-if="!searchForm.reportType === 'inout'"
              v-if="searchForm.reportType !== 'inout'"
           />
           <el-table-column
             label="产品大类"
@@ -207,6 +207,7 @@
             prop="totalStockOut"
             width="100"
             align="center"
              v-if="searchForm.reportType === 'inout'"
           />
           <el-table-column
             label="现在库存"
@@ -215,7 +216,7 @@
           />
           <el-table-column label="来源"
                            prop="recordType"
                            v-if="!searchForm.reportType === 'inout'"
                           v-if="searchForm.reportType !== 'inout'"
                            show-overflow-tooltip>
             <template #default="scope">
               {{ getRecordType(scope.row.recordType) }}
@@ -225,7 +226,7 @@
             label="入库人"
             prop="createBy"
             width="80"
             v-if="!searchForm.reportType === 'inout'"
              v-if="searchForm.reportType !== 'inout'"
             show-overflow-tooltip
           />
        </el-table>
src/views/salesManagement/deliveryLedger/index.vue
@@ -51,13 +51,13 @@
              link 
              type="primary" 
              size="small" 
              :disabled="!isApproved(scope.row.status)"
              :disabled="isApproving(scope.row.status)"
              @click="openForm('edit', scope.row)">编辑</el-button>
            <el-button 
              link 
              type="danger" 
              size="small" 
              :disabled="!isApproved(scope.row.status)"
              :disabled="isApproving(scope.row.status)"
              @click="handleDeleteSingle(scope.row)">删除</el-button>
          </template>
        </el-table-column>
@@ -284,9 +284,9 @@
// 打开弹框
const openForm = async (type, row) => {
  // 编辑时检查审核状态
  if (type === 'edit' && row && !isApproved(row.status)) {
    proxy.$modal.msgWarning("只能编辑审核通过的数据");
  // 编辑时检查审核状态,只有审核中不能编辑
  if (type === 'edit' && row && isApproving(row.status)) {
    proxy.$modal.msgWarning("审核中的数据不能编辑");
    return;
  }
  
@@ -430,10 +430,10 @@
    return;
  }
  
  // 检查选中的行是否都是"审核通过"状态
  const notApprovedRows = selectedRows.value.filter(row => !isApproved(row.status));
  if (notApprovedRows.length > 0) {
    proxy.$modal.msgWarning("只能删除审核通过的数据");
  // 检查选中的行是否有"审核中"状态
  const approvingRows = selectedRows.value.filter(row => isApproving(row.status));
  if (approvingRows.length > 0) {
    proxy.$modal.msgWarning("审核中的数据不能删除");
    return;
  }
  
@@ -456,9 +456,9 @@
// 单个删除
const handleDeleteSingle = (row) => {
  // 检查是否为"审核通过"状态
  if (!isApproved(row.deliveryLedger)) {
    proxy.$modal.msgWarning("只能删除审核通过的数据");
  // 检查是否为"审核中"状态
  if (isApproving(row.status)) {
    proxy.$modal.msgWarning("审核中的数据不能删除");
    return;
  }
  
@@ -635,6 +635,20 @@
  return statusStr === '审核通过' || statusStr === '3';
};
// 检查审核状态是否为"审核中"
const isApproving = (status) => {
  if (status === null || status === undefined || status === '') {
    return false;
  }
  // 如果是数字,1 表示审核中
  if (typeof status === 'number') {
    return status === 1;
  }
  // 如果是字符串
  const statusStr = String(status).trim();
  return statusStr === '审核中' || statusStr === '1';
};
onMounted(() => {
  getList();
});
src/views/salesManagement/indicatorStats/index.vue
@@ -1,82 +1,161 @@
<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>
      <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 :span="8">
          <div class="stat-card">
            <div class="stat-icon" style="background: #f0f9ff;">
              <el-icon :size="30" color="#67c23a"><Tickets /></el-icon>
      <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 :span="8">
          <div class="stat-card">
            <div class="stat-icon" style="background: #fef0f0;">
              <el-icon :size="30" color="#e6a23c"><Van /></el-icon>
      <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-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-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 :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-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 :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 :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 :span="6" style="text-align: right;">
          <el-button type="primary" @click="applyIndicatorFilter">查询</el-button>
          <el-button @click="resetIndicatorFilter">重置</el-button>
          <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">
      <!-- 图表展示区 -->
      <div class="chart-container" v-loading="loading">
        <div ref="indicatorChartRef" class="chart-wrapper"></div>
      </div>
    </el-card>
      <!-- 业绩统计(团队维度,无个人姓名) -->
      <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="销售额">
    <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>
@@ -88,7 +167,7 @@
<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'
@@ -325,10 +404,8 @@
}
const applyIndicatorFilter = async () => {
  await Promise.all([
    fetchTotalStatistics(),
    fetchStatisticsTable()
  ])
  // 筛选条件只影响图表数据,不影响KPI汇总
  await fetchStatisticsTable()
}
const resetIndicatorFilter = () => {
@@ -368,32 +445,314 @@
})
</script>
<style scoped>
<style scoped lang="scss">
.indicator-stats {
  padding: 20px;
  background: #f5f7fa;
  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;
}
.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;
}
.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: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  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: 360px;
    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>
src/views/salesManagement/salesLedger/index.vue
@@ -6,10 +6,6 @@
          <el-input v-model="searchForm.customerName" placeholder="请输入" clearable prefix-icon="Search"
            @change="handleQuery" />
        </el-form-item>
        <el-form-item label="客户合同号:">
          <el-input v-model="searchForm.customerContractNo" placeholder="请输入" clearable prefix-icon="Search"
            @change="handleQuery" />
        </el-form-item>
        <el-form-item label="销售合同号:">
          <el-input v-model="searchForm.salesContractNo" placeholder="请输入" clearable prefix-icon="Search"
            @change="handleQuery" />
@@ -61,14 +57,20 @@
                                                    type="danger">不足</el-tag>
                </template>
              </el-table-column>
                            <el-table-column label="发货状态" prop="shippingStatus" width="140" align="center" show-overflow-tooltip />
                            <el-table-column label="发货状态" width="140" align="center">
                                <template #default="scope">
                                    <el-tag :type="getShippingStatusType(scope.row)" size="small">
                                        {{ getShippingStatusText(scope.row) }}
                                    </el-tag>
                                </template>
                            </el-table-column>
                            <el-table-column label="快递公司" prop="expressCompany" show-overflow-tooltip />
                            <el-table-column label="快递单号" prop="expressNumber" show-overflow-tooltip />
              <el-table-column label="发货车牌" minWidth="100px" align="center">
                <template #default="scope">
                  <div>
                    <el-tag type="success" v-if="scope.row.shippingCarNumber">{{ scope.row.shippingCarNumber }}</el-tag>
                    <el-tag v-else type="info">未发货</el-tag>
                    <el-tag v-else type="info">-</el-tag>
                  </div>
                </template>
              </el-table-column>
@@ -95,7 +97,7 @@
                    link 
                    type="primary" 
                    size="small" 
                    :disabled="scope.row.approveStatus !== 1 || !!scope.row.shippingDate || !!scope.row.shippingCarNumber"
                    :disabled="!canShip(scope.row)"
                    @click="openDeliveryForm(scope.row)">
                    发货
                  </el-button>
@@ -106,7 +108,6 @@
        </el-table-column>
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column label="销售合同号" prop="salesContractNo" width="180" show-overflow-tooltip />
        <el-table-column label="客户合同号" prop="customerContractNo" width="180" show-overflow-tooltip />
        <el-table-column label="客户名称" prop="customerName" width="300" show-overflow-tooltip />
        <el-table-column label="业务员" prop="salesman" width="100" show-overflow-tooltip />
        <el-table-column label="项目名称" prop="projectName" width="180" show-overflow-tooltip />
@@ -148,11 +149,6 @@
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="客户合同号:" prop="customerContractNo">
              <el-input v-model="form.customerContractNo" placeholder="请输入" clearable :disabled="operationType === 'view'"/>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="客户名称:" prop="customerId">
              <el-select v-model="form.customerId" placeholder="请选择" clearable :disabled="operationType === 'view'">
                <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.id">
@@ -163,17 +159,22 @@
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="项目名称:" prop="projectName">
              <el-input v-model="form.projectName" placeholder="请输入" clearable :disabled="operationType === 'view'" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="签订日期:" prop="executionDate">
                            <el-date-picker style="width: 100%" v-model="form.executionDate" value-format="YYYY-MM-DD"
                                                            format="YYYY-MM-DD" type="date" placeholder="请选择" clearable :disabled="operationType === 'view'" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="付款方式">
                            <el-input v-model="form.paymentMethod" placeholder="请输入" clearable :disabled="operationType === 'view'" />
                        </el-form-item>
                    </el-col>
                </el-row>
@@ -195,7 +196,6 @@
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row>
                    <el-form-item label="产品信息:" prop="entryDate">
                        <el-button v-if="operationType !== 'view'" type="primary" @click="openProductForm('add')">添加</el-button>
@@ -1186,19 +1186,15 @@
const openProductForm = async (type, row, index) => {
    productOperationType.value = type;
    productForm.value = {};
    modelOptions.value = []; // 清空规格型号选项
    proxy.resetForm("productFormRef");
    // 确保产品大类数据已加载
    const options = productOptions.value && productOptions.value.length > 0
        ? productOptions.value
        : await getProductOptions();
    if (type === "edit") {
        productForm.value = { ...row };
        productIndex.value = index;
        // 编辑时根据产品大类名称反查 tree 节点 id,并加载规格型号列表
        try {
            const options = productOptions.value && productOptions.value.length > 0
                ? productOptions.value
                : await getProductOptions();
            const categoryId = findNodeIdByLabel(options, productForm.value.productCategory);
            if (categoryId) {
                const models = await modelList({ id: categoryId });
@@ -1215,6 +1211,8 @@
            // 加载失败时保持可编辑,不中断弹窗
            console.error("加载产品规格型号失败", e);
        }
    } else {
        getProductOptions()
    }
    productFormVisible.value = true;
};
@@ -1884,6 +1882,92 @@
    isCalculating.value = false;
};
/**
 * 获取发货状态文本
 * @param row 行数据
 */
const getShippingStatusText = (row) => {
    // 如果已发货(有发货日期或车牌号),显示"已发货"
    if (row.shippingDate || row.shippingCarNumber) {
        return '已发货';
    }
    // 获取发货状态字段
    const status = row.shippingStatus;
    // 如果状态为空或未定义,默认为"待发货"
    if (status === null || status === undefined || status === '') {
        return '待发货';
    }
    // 状态是字符串
    const statusStr = String(status).trim();
    const statusTextMap = {
        '待发货': '待发货',
        '待审核': '待审核',
        '审核中': '审核中',
        '审核拒绝': '审核拒绝',
        '审核通过': '审核通过',
        '已发货': '已发货'
    };
    return statusTextMap[statusStr] || '待发货';
};
/**
 * 获取发货状态标签类型(颜色)
 * @param row 行数据
 */
const getShippingStatusType = (row) => {
    // 如果已发货(有发货日期或车牌号),显示绿色
    if (row.shippingDate || row.shippingCarNumber) {
        return 'success';
    }
    // 获取发货状态字段
    const status = row.shippingStatus;
    // 如果状态为空或未定义,默认为灰色(待发货)
    if (status === null || status === undefined || status === '') {
        return 'info';
    }
    // 状态是字符串
    const statusStr = String(status).trim();
    const typeTextMap = {
        '待发货': 'info',
        '待审核': 'info',
        '审核中': 'warning',
        '审核拒绝': 'danger',
        '审核通过': 'success',
        '已发货': 'success'
    };
    return typeTextMap[statusStr] || 'info';
};
/**
 * 判断是否可以发货
 * 只有在产品状态是充足,发货状态是待发货和审核拒绝的时候才可以发货
 * @param row 行数据
 */
const canShip = (row) => {
    // 产品状态必须是充足(approveStatus === 1)
    if (row.approveStatus !== 1) {
        return false;
    }
    // 获取发货状态
    const shippingStatus = row.shippingStatus;
    // 如果已发货(有发货日期或车牌号),不能再次发货
    if (row.shippingDate || row.shippingCarNumber) {
        return false;
    }
    // 发货状态必须是"待发货"或"审核拒绝"
    const statusStr = shippingStatus ? String(shippingStatus).trim() : '';
    return statusStr === '待发货' || statusStr === '审核拒绝';
};
/**
 * 下载文件
 *
 * @param row 下载文件的相关信息对象
@@ -1900,15 +1984,12 @@
// 打开发货弹框
const openDeliveryForm = (row) => {
    // 校验:只有产品状态为充足且未发货时才能发货
    if (row.approveStatus !== 1) {
        proxy.$modal.msgWarning("产品状态不足,无法发货");
    // 检查是否可以发货
    if (!canShip(row)) {
        proxy.$modal.msgWarning("只有在产品状态是充足,发货状态是待发货或审核拒绝的时候才可以发货");
        return;
    }
    if (row.shippingDate || row.shippingCarNumber) {
        proxy.$modal.msgWarning("该产品已发货,无法重复发货");
        return;
    }
    currentDeliveryRow.value = row;
  deliveryForm.value = {
    type: "货车",