0c116b0f5624786bd06990b86c467be25e2411fd..519211ac232866afe6b081ae4a97916ad5f1d7d2
4 小时以前 spring
fix: 排名样式修改
519211 对比 | 目录
4 小时以前 spring
fix: 样式调整
0b8b77 对比 | 目录
4 小时以前 spring
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
6c7375 对比 | 目录
4 小时以前 spring
fix: 产品大类样式优化
040503 对比 | 目录
4 小时以前 gongchunyi
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
efaf84 对比 | 目录
4 小时以前 gongchunyi
feat: BI大屏接口对接
d54e45 对比 | 目录
4 小时以前 spring
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
e6e74f 对比 | 目录
4 小时以前 spring
fix: 完成产品大类样式编写,接口联调
d9a327 对比 | 目录
4 小时以前 huminmin
迁移库存解冻/冻结
3d2b83 对比 | 目录
5 小时以前 spring
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
107778 对比 | 目录
5 小时以前 spring
fix: 基础大屏添加同比
23c9b5 对比 | 目录
6 小时以前 gaoluyang
Merge remote-tracking branch 'origin/dev_New' into dev_New
684e61 对比 | 目录
6 小时以前 gaoluyang
进销存升级 1.供应商往来查询接口调整
1c172c 对比 | 目录
6 小时以前 spring
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
fd1ab3 对比 | 目录
6 小时以前 spring
fix: 基础大屏页面重构90%
b7604d 对比 | 目录
6 小时以前 gaoluyang
进销存升级 1.优化供应商档案页面
b6abea 对比 | 目录
7 小时以前 gaoluyang
进销存-升级 1.巡检管理修改上传附件 2.新增销售台账是产品大类查询修改 3.销售台账发货逻辑判断修改,状态展示优化 4.采购审批信息展示优化 5....
003924 对比 | 目录
8 小时以前 huminmin
仓储物流:修改库存报表显示
4e919b 对比 | 目录
10 小时以前 huminmin
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
be7fb5 对比 | 目录
10 小时以前 huminmin
仓储物流:下载库存导入模板放到导入中
991f68 对比 | 目录
10 小时以前 gaoluyang
Merge remote-tracking branch 'origin/dev_New' into dev_New
71fa9d 对比 | 目录
10 小时以前 gaoluyang
进销存-升级 1.巡检管理修改上传附件 2.新增销售台账是产品大类查询修改 3.销售台账发货逻辑判断修改,状态展示优化
9844f4 对比 | 目录
11 小时以前 yaowanxin
营销管理—销售台账:客户合同号去掉,新增时的客户合同号输入框同步并去掉
d392a3 对比 | 目录
11 小时以前 spring
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
5a6097 对比 | 目录
11 小时以前 spring
fix: 设备保养添加附件上传功能
d68ca5 对比 | 目录
11 小时以前 huminmin
仓储物流:库存报表列表
6d1e99 对比 | 目录
12 小时以前 gongchunyi
fix: 新增销售台账时,产品选择为空
9b74af 对比 | 目录
12 小时以前 gaoluyang
进销存-升级 1.巡检管理修改上传附件 2.新增销售台账是产品大类查询修改
922b1b 对比 | 目录
12 小时以前 gaoluyang
进销存-升级 1.巡检管理修改
98adb0 对比 | 目录
已添加12个文件
已修改26个文件
8704 ■■■■■ 文件已修改
src/api/equipmentManagement/maintenanceTaskFile.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockInventory.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockUninventory.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/procurementManagement/paymentLedger.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/procurementManagement/procurementLedger.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/viewIndex.js 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/BI/玫瑰图边框.png 补丁 | 查看 | 原始文档 | 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 32 ●●●●● 补丁 | 查看 | 原始文档 | 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/equipmentManagement/inspectionManagement/components/formDia.vue 391 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/viewFiles.vue 78 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/index.vue 507 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/index.vue 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/FrozenAndThaw.vue 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Import.vue 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/New.vue 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Qualified.vue 39 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Subtract.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Unqualified.vue 45 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockReport/index.vue 333 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/paymentLedger/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/DateTypeSwitch.vue 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/PanelHeader.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/center-bottom.vue 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/center-top.vue 519 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/left-bottom.vue 244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue 329 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue 358 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/index.vue 2059 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/index0.vue 2036 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/deliveryLedger/index.vue 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/indicatorStats/index.vue 537 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 139 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/maintenanceTaskFile.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
import request from "@/utils/request";
// æŸ¥è¯¢ä¿å…»ä»»åŠ¡é™„ä»¶åˆ—è¡¨
export function listMaintenanceTaskFiles(query) {
  return request({
    url: "/maintenanceTaskFile/listPage",
    method: "get",
    params: query,
  });
}
// æ–°å¢žä¿å…»ä»»åС附件
export function addMaintenanceTaskFile(data) {
  return request({
    url: "/maintenanceTaskFile/add",
    method: "post",
    data,
  });
}
// åˆ é™¤ä¿å…»ä»»åС附件
export function delMaintenanceTaskFile(id) {
  return request({
    url: "/maintenanceTaskFile/del",
    method: "delete",
    data: Array.isArray(id) ? id : [id],
  });
}
src/api/inventoryManagement/stockInventory.js
@@ -26,12 +26,37 @@
    });
};
export const exportStockInventory = (params) => {
export const getStockInventoryReportList = (params) => {
    return request({
        url: "/stockInventory/exportStockInventory",
        url: "/stockInventory/stockInventoryPage",
        method: "get",
        params,
    });
};
export const getStockInventoryInAndOutReportList = (params) => {
    return request({
        url: "/stockInventory/stockInAndOutRecord",
        method: "get",
        params,
    });
};
// å†»ç»“库存记录
export const frozenStockInventory = (params) => {
    return request({
        url: "/stockInventory/frozenStock",
        method: "post",
        data: params,
    });
};
// è§£å†»åº“存记录
export const thawStockInventory = (params) => {
    return request({
        url: "/stockInventory/thawStock",
        method: "post",
        data: params,
    });
};
src/api/inventoryManagement/stockUninventory.js
@@ -25,3 +25,21 @@
        data: params,
    });
};
// å†»ç»“库存记录
export const frozenStockUninventory = (params) => {
    return request({
        url: "/stockUninventory/frozenStock",
        method: "post",
        data: params,
    });
};
// è§£å†»åº“存记录
export const thawStockUninventory = (params) => {
    return request({
        url: "/stockUninventory/thawStock",
        method: "post",
        data: params,
    });
};
src/api/procurementManagement/paymentLedger.js
@@ -4,7 +4,7 @@
// åˆ†é¡µæŸ¥è¯¢
export function paymentLedgerList(query) {
  return request({
    url: "/purchase/paymentRegistration/paymentLedgerList",
    url: "/purchase/paymentRegistration/supplierNameListPage",
    method: "get",
    params: query,
  });
@@ -13,7 +13,8 @@
// åˆ†é¡µæŸ¥è¯¢
export function paymentRecordList(supplierId) {
  return request({
    url: "/purchase/paymentRegistration/getPaymentRecordList/" + supplierId,
    url: "/purchase/paymentRegistration/supplierNameListPageDetails",
    method: "get",
    params: supplierId,
  });
}
src/api/procurementManagement/procurementLedger.js
@@ -114,4 +114,12 @@
        method: "delete",
        data: id,
    });
}
// æŸ¥è¯¢é‡‡è´­è¯¦æƒ…
export function getPurchaseByCode(id) {
    return request({
        url: "/purchase/ledger/getPurchaseByCode",
        method: "get",
        params: id,
    });
}
src/api/viewIndex.js
@@ -57,9 +57,60 @@
//在制品周转情况
//home/workInProcessTurnover
export const getWorkInProcessTurnover= ()=>{
export const getWorkInProcessTurnover = () => {
    return request({
        url: '/home/workInProcessTurnover',
        method: 'get'
    })
}
}
// å®¢æˆ·è¥æ”¶è´¡çŒ®æ•°å€¼åˆ†æž
export const customerRevenueAnalysis = (params) => {
    return request({
        url: '/home/customerRevenueAnalysis',
        method: 'get',
        params
    })
}
// å‘˜å·¥-客户-供应商总数
export const summaryStatistics = () => {
    return request({
        url: '/home/summaryStatistics',
        method: 'get'
    })
}
// å„部门人员分布
export const deptStaffDistribution = () => {
    return request({
        url: '/home/deptStaffDistribution',
        method: 'get'
    })
}
// ä¾›åº”商采购排名
export const supplierPurchaseRanking = (query) => {
    return request({
        url: '/home/supplierPurchaseRanking',
        method: 'get',
        params: query
    })
}
// å®¢æˆ·é‡‘额贡献排名
export const customerContributionRanking = (query) => {
    return request({
        url: '/home/customerContributionRanking',
        method: 'get',
        params: query
    })
}
// å„产品大类分布
export const productCategoryDistribution = () => {
    return request({
        url: '/home/productCategoryDistribution',
        method: 'get'
    })
}
src/assets/BI/õ¹åͼ±ß¿ò.png
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/equipmentManagement/inspectionManagement/components/formDia.vue
@@ -1,116 +1,102 @@
<template>
    <div>
        <el-dialog :title="operationType === 'add' ? '新增巡检任务' : '编辑巡检任务'"
                             v-model="dialogVisitable" width="800px" @close="cancel">
            <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
                <el-row>
                    <el-col :span="12">
                        <el-form-item label="设备名称" prop="taskId">
                            <el-select v-model="form.taskId" @change="setDeviceModel" filterable>
                                <el-option
                                    v-for="(item, index) in deviceOptions"
                                    :key="index"
                                    :label="item.deviceName"
                                    :value="item.id"
                                ></el-option>
                            </el-select>
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="巡检人" prop="inspector">
                            <el-select v-model="form.inspector"                 filterable
                                                 default-first-option
                                                 :reserve-keyword="false" placeholder="请选择" multiple clearable>
                                <el-option v-for="item in userList" :label="item.nickName" :value="item.userId" :key="item.userId"/>
                            </el-select>
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row>
                    <el-col :span="12">
                        <el-form-item label="备注" prop="remarks">
                            <el-input v-model="form.remarks" placeholder="请输入备注" type="textarea" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="登记时间" prop="dateStr">
                            <el-date-picker
                                v-model="form.dateStr"
                                type="date"
                                placeholder="选择登记日期"
                                format="YYYY-MM-DD"
                                value-format="YYYY-MM-DD"
                                style="width: 100%"
                            />
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row>
                    <el-col :span="12">
                        <el-form-item label="任务频率" prop="frequencyType">
                            <el-select v-model="form.frequencyType" placeholder="请选择" clearable>
                                <el-option label="每日" value="DAILY"/>
                                <el-option label="每周" value="WEEKLY"/>
                                <el-option label="每月" value="MONTHLY"/>
                                <!-- <el-option label="季度" value="QUARTERLY"/> -->
                            </el-select>
                        </el-form-item>
                    </el-col>
                    <el-col :span="12" v-if="form.frequencyType === 'DAILY' && form.frequencyType">
                        <el-form-item label="日期" prop="frequencyDetail">
                            <el-time-picker v-model="form.frequencyDetail" placeholder="选择时间" format="HH:mm"
                                                            value-format="HH:mm" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12" v-if="form.frequencyType === 'WEEKLY' && form.frequencyType">
                        <el-form-item label="日期" prop="frequencyDetail">
                            <el-select v-model="form.week" placeholder="请选择" clearable style="width: 50%">
                                <el-option label="周一" value="MON"/>
                                <el-option label="周二" value="TUE"/>
                                <el-option label="周三" value="WED"/>
                                <el-option label="周四" value="THU"/>
                                <el-option label="周五" value="FRI"/>
                                <el-option label="周六" value="SAT"/>
                                <el-option label="周日" value="SUN"/>
                            </el-select>
                            <el-time-picker v-model="form.time" placeholder="选择时间" format="HH:mm"
                                                            value-format="HH:mm"  style="width: 50%"/>
                        </el-form-item>
                    </el-col>
                    <el-col :span="12" v-if="form.frequencyType === 'MONTHLY' && form.frequencyType">
                        <el-form-item label="日期" prop="frequencyDetail">
                            <el-date-picker
                                v-model="form.frequencyDetail"
                                type="datetime"
                                clearable
                                placeholder="选择开始日期"
                                format="DD,HH:mm"
                                value-format="DD,HH:mm"
                            />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12" v-if="form.frequencyType === 'QUARTERLY' && form.frequencyType">
                        <el-form-item label="日期" prop="frequencyDetail">
                            <el-date-picker
                                v-model="form.frequencyDetail"
                                type="datetime"
                                clearable
                                placeholder="选择开始日期"
                                format="MM,DD,HH:mm"
                                value-format="MM,DD,HH:mm"
                            />
                        </el-form-item>
                    </el-col>
                </el-row>
            </el-form>
            <template #footer>
                <div class="dialog-footer">
                    <el-button @click="cancel">取消</el-button>
                    <el-button type="primary" @click="submitForm">保存</el-button>
                </div>
            </template>
        </el-dialog>
    </div>
  <div>
    <el-dialog :title="operationType === 'add' ? '新增巡检任务' : '编辑巡检任务'"
               v-model="dialogVisitable" width="800px" @close="cancel">
      <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
        <el-row>
          <el-col :span="12">
            <el-form-item label="设备名称" prop="taskId">
              <el-select v-model="form.taskId" @change="setDeviceModel">
                <el-option
                  v-for="(item, index) in deviceOptions"
                  :key="index"
                  :label="item.deviceName"
                  :value="item.id"
                ></el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="巡检人" prop="inspector">
              <el-select v-model="form.inspector" placeholder="请选择" multiple clearable>
                <el-option v-for="item in userList" :label="item.nickName" :value="item.userId" :key="item.userId"/>
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="备注" prop="remarks">
              <el-input v-model="form.remarks" placeholder="请输入备注" type="textarea" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="任务频率" prop="frequencyType">
              <el-select v-model="form.frequencyType" placeholder="请选择" clearable>
                <el-option label="每日" value="DAILY"/>
                <el-option label="每周" value="WEEKLY"/>
                <el-option label="每月" value="MONTHLY"/>
                <!-- <el-option label="季度" value="QUARTERLY"/> -->
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="form.frequencyType === 'DAILY' && form.frequencyType">
            <el-form-item label="日期" prop="frequencyDetail">
              <el-time-picker v-model="form.frequencyDetail" placeholder="选择时间" format="HH:mm"
                              value-format="HH:mm" />
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="form.frequencyType === 'WEEKLY' && form.frequencyType">
            <el-form-item label="日期" prop="frequencyDetail">
              <el-select v-model="form.week" placeholder="请选择" clearable style="width: 50%">
                <el-option label="周一" value="MON"/>
                <el-option label="周二" value="TUE"/>
                <el-option label="周三" value="WED"/>
                <el-option label="周四" value="THU"/>
                <el-option label="周五" value="FRI"/>
                <el-option label="周六" value="SAT"/>
                <el-option label="周日" value="SUN"/>
              </el-select>
              <el-time-picker v-model="form.time" placeholder="选择时间" format="HH:mm"
                              value-format="HH:mm"  style="width: 50%"/>
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="form.frequencyType === 'MONTHLY' && form.frequencyType">
            <el-form-item label="日期" prop="frequencyDetail">
              <el-date-picker
                  v-model="form.frequencyDetail"
                  type="datetime"
                  clearable
                  placeholder="选择开始日期"
                  format="DD,HH:mm"
                  value-format="DD,HH:mm"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="form.frequencyType === 'QUARTERLY' && form.frequencyType">
            <el-form-item label="日期" prop="frequencyDetail">
              <el-date-picker
                  v-model="form.frequencyDetail"
                  type="datetime"
                  clearable
                  placeholder="选择开始日期"
                  format="MM,DD,HH:mm"
                  value-format="MM,DD,HH:mm"
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="cancel">取消</el-button>
          <el-button type="primary" @click="submitForm">保存</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
@@ -127,27 +113,26 @@
const operationType = ref('add');
const deviceOptions = ref([]);
const data = reactive({
    form: {
        taskId: undefined,
        taskName: undefined,
        inspector: '',
        inspectorIds: '',
        remarks: '',
        frequencyType: '',
        frequencyDetail: '',
        week: '',
        time: '',
        dateStr: ''
    },
  form: {
    taskId: undefined,
    taskName: undefined,
    inspector: '',
    inspectorIds: '',
    remarks: '',
    frequencyType: '',
    frequencyDetail: '',
    week: '',
    time: ''
  },
    rules: {
        taskId: [{ required: true, message: "请选择设备", trigger: "change" },],
        inspector: [{ required: true, message: "请输入巡检人", trigger: "blur" },],
        dateStr: [{ required: true, message: "请选择登记时间", trigger: "change" }],
        frequencyType: [{ required: true, message: "请选择任务频率", trigger: "change" }],
        frequencyDetail: [
            {
                required: true,
                message: "请选择日期",
            {
                required: true,
                message: "请选择日期",
                trigger: "change",
                validator: (rule, value, callback) => {
                    if (!form.value.frequencyType) {
@@ -171,9 +156,9 @@
            }
        ],
        week: [
            {
                required: true,
                message: "请选择星期",
            {
                required: true,
                message: "请选择星期",
                trigger: "change",
                validator: (rule, value, callback) => {
                    if (form.value.frequencyType === 'WEEKLY' && !value) {
@@ -185,9 +170,9 @@
            }
        ],
        time: [
            {
                required: true,
                message: "请选择时间",
            {
                required: true,
                message: "请选择时间",
                trigger: "change",
                validator: (rule, value, callback) => {
                    if (form.value.frequencyType === 'WEEKLY' && !value) {
@@ -204,95 +189,95 @@
const userList = ref([])
const loadDeviceName = async () => {
    const { data } = await getDeviceLedger();
    deviceOptions.value = data;
  const { data } = await getDeviceLedger();
  deviceOptions.value = data;
};
const setDeviceModel = (id) => {
    const option = deviceOptions.value.find((item) => item.id === id);
    if (option) {
        form.value.taskName = option.deviceName;
    }
  const option = deviceOptions.value.find((item) => item.id === id);
  if (option) {
    form.value.taskName = option.deviceName;
  }
}
// æ‰“开弹框
const openDialog = async (type, row) => {
    dialogVisitable.value = true
    operationType.value = type
    // é‡ç½®è¡¨å•
    resetForm();
    // åŠ è½½ç”¨æˆ·åˆ—è¡¨
    userListNoPageByTenantId().then((res) => {
        userList.value = res.data;
    });
    // åŠ è½½è®¾å¤‡åˆ—è¡¨
    await loadDeviceName();
    if (type === 'edit' && row) {
        form.value = {...row}
        form.value.inspector = form.value.inspectorIds.split(',').map(Number)
        // å¦‚果有设备ID,自动设置设备信息
        if (form.value.taskId) {
            setDeviceModel(form.value.taskId);
        }
    }
  dialogVisitable.value = true
  operationType.value = type
  // é‡ç½®è¡¨å•
  resetForm();
  // åŠ è½½ç”¨æˆ·åˆ—è¡¨
  userListNoPageByTenantId().then((res) => {
    userList.value = res.data;
  });
  // åŠ è½½è®¾å¤‡åˆ—è¡¨
  await loadDeviceName();
  if (type === 'edit' && row) {
    form.value = {...row}
    form.value.inspector = form.value.inspectorIds.split(',').map(Number)
    // å¦‚果有设备ID,自动设置设备信息
    if (form.value.taskId) {
      setDeviceModel(form.value.taskId);
    }
  }
}
// å…³é—­å¯¹è¯æ¡†
const cancel = () => {
    resetForm()
    dialogVisitable.value = false
    emit('closeDia')
  resetForm()
  dialogVisitable.value = false
  emit('closeDia')
}
// é‡ç½®è¡¨å•函数
const resetForm = () => {
    if (proxy.$refs.formRef) {
        proxy.$refs.formRef.resetFields()
    }
    // é‡ç½®è¡¨å•数据确保设备信息正确重置
    form.value = {
        taskId: undefined,
        taskName: undefined,
        inspector: '',
        inspectorIds: '',
        remarks: '',
        frequencyType: '',
        frequencyDetail: '',
        week: '',
        time: ''
    }
  if (proxy.$refs.formRef) {
    proxy.$refs.formRef.resetFields()
  }
  // é‡ç½®è¡¨å•数据确保设备信息正确重置
  form.value = {
    taskId: undefined,
    taskName: undefined,
    inspector: '',
    inspectorIds: '',
    remarks: '',
    frequencyType: '',
    frequencyDetail: '',
    week: '',
    time: ''
  }
}
// æäº¤è¡¨å•
const submitForm = () => {
    proxy.$refs["formRef"].validate(async valid => {
        if (valid) {
            try {
                form.value.inspectorIds = form.value.inspector.join(',')
                delete form.value.inspector
                if (form.value.frequencyType === 'WEEKLY') {
                    let frequencyDetail = ''
                    frequencyDetail = form.value.week + ',' + form.value.time
                    form.value.frequencyDetail = frequencyDetail
                }
                let res = await userStore.getInfo()
                form.value.registrantId = res.user.userId
                await addOrEditTimingTask(form.value)
                cancel()
                proxy.$modal.msgSuccess('提交成功')
            } catch (error) {
                proxy.$modal.msgError('提交失败,请重试')
            }
        }
    })
  proxy.$refs["formRef"].validate(async valid => {
    if (valid) {
      try {
        form.value.inspectorIds = form.value.inspector.join(',')
        delete form.value.inspector
        if (form.value.frequencyType === 'WEEKLY') {
          let frequencyDetail = ''
          frequencyDetail = form.value.week + ',' + form.value.time
          form.value.frequencyDetail = frequencyDetail
        }
        let res = await userStore.getInfo()
        form.value.registrantId = res.user.userId
        await addOrEditTimingTask(form.value)
        cancel()
        proxy.$modal.msgSuccess('提交成功')
      } catch (error) {
        proxy.$modal.msgError('提交失败,请重试')
      }
    }
  })
}
defineExpose({ openDialog })
</script>
src/views/equipmentManagement/inspectionManagement/components/viewFiles.vue
@@ -32,7 +32,7 @@
        
        <!-- ç”Ÿäº§åŽ -->
        <div class="form-container">
          <div class="title">生产中</div>
          <div class="title">生产后</div>
          
          <!-- å›¾ç‰‡åˆ—表 -->
          <div style="display: flex; flex-wrap: wrap;">
@@ -59,7 +59,7 @@
        
        <!-- ç”Ÿäº§é—®é¢˜ -->
        <div class="form-container">
          <div class="title">生产后</div>
          <div class="title">生产问题</div>
          
          <!-- å›¾ç‰‡åˆ—表 -->
          <div style="display: flex; flex-wrap: wrap;">
@@ -100,7 +100,7 @@
        
        <!-- è§†é¢‘ -->
        <div v-else-if="mediaType === 'video'" style="position: relative;">
          <Video
          <video
              :src="mediaList[currentMediaIndex]"
              autoplay
              controls
@@ -114,6 +114,7 @@
<script setup>
import { ref } from 'vue';
import VueEasyLightbox from 'vue-easy-lightbox';
const { proxy } = getCurrentInstance();
// æŽ§åˆ¶å¼¹çª—显示
const dialogVisitable = ref(false);
@@ -133,26 +134,83 @@
const currentMediaIndex = ref(0);
const mediaList = ref([]); // å­˜å‚¨å½“前要查看的媒体列表(含图片和视频对象)
const mediaType = ref('image'); // image | video
const javaApi = proxy.javaApi;
// å¤„理 URL:将 Windows è·¯å¾„转换为可访问的 URL
function processFileUrl(fileUrl) {
  if (!fileUrl) return '';
  // å¦‚æžœ URL æ˜¯ Windows è·¯å¾„格式(包含反斜杠),需要转换
  if (fileUrl && fileUrl.indexOf('\\') > -1) {
    // æŸ¥æ‰¾ uploads å…³é”®å­—的位置,从那里开始提取相对路径
    const uploadsIndex = fileUrl.toLowerCase().indexOf('uploads');
    if (uploadsIndex > -1) {
      // ä»Ž uploads å¼€å§‹æå–路径,并将反斜杠替换为正斜杠
      const relativePath = fileUrl.substring(uploadsIndex).replace(/\\/g, '/');
      fileUrl = '/' + relativePath;
    } else {
      // å¦‚果没有找到 uploads,提取最后一个目录和文件名
      const parts = fileUrl.split('\\');
      const fileName = parts[parts.length - 1];
      fileUrl = '/uploads/' + fileName;
    }
  }
  // ç¡®ä¿æ‰€æœ‰éž http å¼€å¤´çš„ URL éƒ½æ‹¼æŽ¥ baseUrl
  if (fileUrl && !fileUrl.startsWith('http')) {
    // ç¡®ä¿è·¯å¾„以 / å¼€å¤´
    if (!fileUrl.startsWith('/')) {
      fileUrl = '/' + fileUrl;
    }
    // æ‹¼æŽ¥ baseUrl
    fileUrl = javaApi + fileUrl;
  }
  return fileUrl;
}
// å¤„理每一类数据:分离图片和视频
function processItems(items) {
  const images = [];
  const videos = [];
  // æ£€æŸ¥ items æ˜¯å¦å­˜åœ¨ä¸”为数组
  if (!items || !Array.isArray(items)) {
    return { images, videos };
  }
  items.forEach(item => {
    if (item.contentType?.startsWith('image/')) {
      images.push(item.url);
    } else if (item.contentType?.startsWith('video/')) {
      videos.push(item.url);
    if (!item || !item.url) return;
    // å¤„理文件 URL
    const fileUrl = processFileUrl(item.url);
    // æ ¹æ®æ–‡ä»¶æ‰©å±•名判断是图片还是视频
    const urlLower = fileUrl.toLowerCase();
    if (urlLower.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/)) {
      images.push(fileUrl);
    } else if (urlLower.match(/\.(mp4|avi|mov|wmv|flv|mkv|webm)$/)) {
      videos.push(fileUrl);
    } else if (item.contentType) {
      // å¦‚果有 contentType,使用 contentType åˆ¤æ–­
      if (item.contentType.startsWith('image/')) {
        images.push(fileUrl);
      } else if (item.contentType.startsWith('video/')) {
        videos.push(fileUrl);
      }
    }
  });
  return { images, videos };
}
// æ‰“开弹窗并加载数据
const openDialog = async (row) => {
  const { images: beforeImgs, videos: beforeVids } = processItems(row.beforeProduction);
  const { images: afterImgs, videos: afterVids } = processItems(row.afterProduction);
  const { images: issueImgs, videos: issueVids } = processItems(row.productionIssues);
  // ä½¿ç”¨æ­£ç¡®çš„字段名:commonFileListBefore, commonFileListAfter
  // productionIssues å¯èƒ½ä¸å­˜åœ¨ï¼Œä½¿ç”¨ç©ºæ•°ç»„
  const { images: beforeImgs, videos: beforeVids } = processItems(row.commonFileListBefore || []);
  const { images: afterImgs, videos: afterVids } = processItems(row.commonFileListAfter || []);
  const { images: issueImgs, videos: issueVids } = processItems(row.productionIssues || []);
  
  beforeProductionImgs.value = beforeImgs;
  beforeProductionVideos.value = beforeVids;
src/views/equipmentManagement/inspectionManagement/index.vue
@@ -1,78 +1,77 @@
<template>
    <div class="app-container">
        <el-form :inline="true" :model="queryParams" class="search-form">
            <el-form-item label="巡检任务名称">
                <el-input
                    v-model="queryParams.taskName"
                    placeholder="请输入巡检任务名称"
                    clearable
                    :style="{ width: '100%' }"
                />
            </el-form-item>
            <el-form-item>
                <el-button type="primary" @click="handleQuery">查询</el-button>
                <el-button @click="resetQuery">重置</el-button>
            </el-form-item>
        </el-form>
        <el-card>
            <div style="display: flex;flex-direction: row;justify-content: space-between;margin-bottom: 10px;">
                <el-radio-group v-model="activeRadio" @change="radioChange">
                    <el-radio-button v-for="tab in radios"
                                                     :key="tab.name"
                                                     :label="tab.label"
                                                     :value="tab.name"/>
                </el-radio-group>
                <!-- æ“ä½œæŒ‰é’®åŒº -->
                <el-space v-if="activeRadio !== 'task'">
                    <el-button type="primary" :icon="Plus" @click="handleAdd(undefined)">新建</el-button>
                    <el-button type="danger" :icon="Delete" @click="handleDelete">删除</el-button>
                    <el-button @click="handleOut">导出</el-button>
                </el-space>
                <el-space v-else>
                    <el-button @click="handleOut">导出</el-button>
                </el-space>
            </div>
            <div>
                <div>
                    <PIMTable :table-loading="tableLoading"
                                        :table-data="tableData"
                                        :column="tableColumns"
                                        @selection-change="handleSelectionChange"
                                        :is-selection="true"
                                        :border="true"
                                        :table-style="{ width: '100%', height: 'calc(100vh - 23em)' }"
                                        :page="{
          current: pageNum,
          size: pageSize,
          total: total,
        }"
                                        @pagination="pagination"
                    >
                        <template #inspector="{ row }">
                            <div class="person-tags">
                                <!-- è°ƒè¯•信息,上线时删除 -->
                                <!-- {{ console.log('inspector data:', row.inspector) }} -->
                                <template v-if="row.inspector && row.inspector.length > 0">
                                    <el-tag
                                        v-for="(person, index) in row.inspector"
                                        :key="index"
                                        size="small"
                                        type="primary"
                                        class="person-tag"
                                    >
                                        {{ person }}
                                    </el-tag>
                                </template>
                                <span v-else class="no-data">--</span>
                            </div>
                        </template>
                    </PIMTable>
                </div>
            </div>
        </el-card>
        <form-dia ref="formDia" @closeDia="handleQuery"></form-dia>
        <view-files ref="viewFiles"></view-files>
    </div>
  <div class="app-container">
    <el-form :inline="true" :model="queryParams" class="search-form">
      <el-form-item label="巡检任务名称">
        <el-input
            v-model="queryParams.taskName"
            placeholder="请输入巡检任务名称"
            clearable
            style="width: 200px "
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="handleQuery">查询</el-button>
        <el-button @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <el-card>
      <div style="display: flex;flex-direction: row;justify-content: space-between;margin-bottom: 10px;">
        <el-radio-group v-model="activeRadio" @change="radioChange">
          <el-radio-button v-for="tab in radios"
                           :key="tab.name"
                           :label="tab.label"
                           :value="tab.name"/>
        </el-radio-group>
        <!-- æ“ä½œæŒ‰é’®åŒº -->
        <el-space v-if="activeRadio !== 'task'">
          <el-button type="primary" :icon="Plus" @click="handleAdd(undefined)">新建</el-button>
          <el-button type="danger" :icon="Delete" @click="handleDelete">删除</el-button>
          <el-button @click="handleOut">导出</el-button>
        </el-space>
        <el-space v-else>
          <el-button @click="handleOut">导出</el-button>
        </el-space>
      </div>
      <div>
        <PIMTable :table-loading="tableLoading"
                :table-data="tableData"
                :column="tableColumns"
                @selection-change="handleSelectionChange"
                @pagination="handlePagination"
                :is-selection="true"
                :border="true"
                :page="{
                  current: pageNum,
                  size: pageSize,
                  total: total,
                  layout: 'total, sizes, prev, pager, next, jumper'
                }"
                :table-style="{ width: '100%', height: 'calc(100vh - 23em)' }"
        >
          <template #inspector="{ row }">
            <div class="person-tags">
              <!-- è°ƒè¯•信息,上线时删除 -->
              <!-- {{ console.log('inspector data:', row.inspector) }} -->
              <template v-if="row.inspector && row.inspector.length > 0">
                <el-tag
                  v-for="(person, index) in row.inspector"
                  :key="index"
                  size="small"
                  type="primary"
                  class="person-tag"
                >
                  {{ person }}
                </el-tag>
              </template>
              <span v-else class="no-data">--</span>
            </div>
          </template>
        </PIMTable>
      </div>
    </el-card>
    <form-dia ref="formDia" @closeDia="handleQuery"></form-dia>
    <view-files ref="viewFiles"></view-files>
  </div>
</template>
<script setup>
@@ -81,16 +80,15 @@
import { ElMessageBox } from "element-plus";
// ç»„件引入
import Pagination from "@/components/Pagination/index.vue";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import FormDia from "@/views/equipmentManagement/inspectionManagement/components/formDia.vue";
import ViewFiles from "@/views/equipmentManagement/inspectionManagement/components/viewFiles.vue";
// æŽ¥å£å¼•å…¥
import {
    delTimingTask,
    inspectionTaskList,
    timingTaskList
  delTimingTask,
  inspectionTaskList,
  timingTaskList
} from "@/api/inspectionManagement/index.js";
// å…¨å±€å˜é‡
@@ -100,14 +98,14 @@
// æŸ¥è¯¢å‚æ•°
const queryParams = reactive({
    taskName: "",
  taskName: "",
});
// å•选框配置
const activeRadio = ref("taskManage");
const radios = reactive([
    { name: "taskManage", label: "定时任务管理" },
    { name: "task", label: "定时任务记录" },
  { name: "taskManage", label: "定时任务管理" },
  { name: "task", label: "定时任务记录" },
]);
// è¡¨æ ¼æ•°æ®
@@ -122,233 +120,234 @@
// åˆ—配置
const columns = ref([
    { prop: "taskName", label: "巡检任务名称", minWidth: 160 },
    { prop: "remarks", label: "备注", minWidth: 150 },
    { prop: "inspector", label: "执行巡检人", minWidth: 150, slot: "inspector" },
    {
        prop: "frequencyType",
        label: "频次",
        minWidth: 150,
        formatData: (cell) => ({
            DAILY: "每日",
            WEEKLY: "每周",
            MONTHLY: "每月",
            QUARTERLY: "季度"
        }[cell] || "")
    },
    {
        prop: "frequencyDetail",
        label: "开始日期与时间",
        minWidth: 150,
        formatter: (row, column, cellValue) => {
            // å…ˆåˆ¤æ–­æ˜¯å¦æ˜¯å­—符串
            if (typeof cellValue !== 'string') return '';
            let val = cellValue;
            const replacements = {
                MON: '周一',
                TUE: '周二',
                WED: '周三',
                THU: '周四',
                FRI: '周五',
                SAT: '周六',
                SUN: '周日'
            };
            // ä½¿ç”¨æ­£åˆ™ä¸€æ¬¡æ€§æ›¿æ¢æ‰€æœ‰åŒ¹é…é¡¹
            return val.replace(/MON|TUE|WED|THU|FRI|SAT|SUN/g, match => replacements[match]);
        }
    },
    { prop: "registrant", label: "登记人", minWidth: 100 },
    { prop: "dateStr", label: "登记日期", minWidth: 100 },
  { prop: "taskName", label: "巡检任务名称", minWidth: 160 },
  { prop: "remarks", label: "备注", minWidth: 150 },
  { prop: "inspector", label: "执行巡检人", minWidth: 150, slot: "inspector" },
  {
    prop: "frequencyType",
    label: "频次",
    minWidth: 150,
    formatter: (_, __, val) => ({
      DAILY: "每日",
      WEEKLY: "每周",
      MONTHLY: "每月",
      QUARTERLY: "季度"
    }[val] || "")
  },
  {
    prop: "frequencyDetail",
    label: "开始日期与时间",
    minWidth: 150,
    formatter: (row, column, cellValue) => {
      // å…ˆåˆ¤æ–­æ˜¯å¦æ˜¯å­—符串
      if (typeof cellValue !== 'string') return '';
      let val = cellValue;
      const replacements = {
        MON: '周一',
        TUE: '周二',
        WED: '周三',
        THU: '周四',
        FRI: '周五',
        SAT: '周六',
        SUN: '周日'
      };
      // ä½¿ç”¨æ­£åˆ™ä¸€æ¬¡æ€§æ›¿æ¢æ‰€æœ‰åŒ¹é…é¡¹
      return val.replace(/MON|TUE|WED|THU|FRI|SAT|SUN/g, match => replacements[match]);
    }
  },
  { prop: "registrant", label: "登记人", minWidth: 100 },
  { prop: "createTime", label: "登记日期", minWidth: 100 },
]);
// æ“ä½œåˆ—配置
const getOperationColumn = (operations) => {
    if (!operations || operations.length === 0) return null;
    const operationConfig = {
        label: "操作",
        width: 130,
        fixed: "right",
        dataType: "action",
        operation: operations.map(op => {
            switch (op) {
                case 'edit':
                    return {
                        name: "编辑",
                        clickFun: handleAdd,
                        color: "#409EFF"
                    };
                case 'viewFile':
                    return {
                        name: "查看附件",
                        clickFun: viewFile,
                        color: "#67C23A"
                    };
                default:
                    return null;
            }
        }).filter(Boolean)
    };
    return operationConfig;
  if (!operations || operations.length === 0) return null;
  const operationConfig = {
    label: "操作",
    width: 130,
    fixed: "right",
    dataType: "action",
    operation: operations.map(op => {
      switch (op) {
        case 'edit':
          return {
            name: "编辑",
            clickFun: handleAdd,
            color: "#409EFF"
          };
        case 'viewFile':
          return {
            name: "查看附件",
            clickFun: viewFile,
            color: "#67C23A"
          };
        default:
          return null;
      }
    }).filter(Boolean)
  };
  return operationConfig;
};
onMounted(() => {
    radioChange('taskManage');
  radioChange('taskManage');
});
// å•选变化
const radioChange = (value) => {
    if (value === "taskManage") {
        const operationColumn = getOperationColumn(['edit']);
        tableColumns.value = [...columns.value, ...(operationColumn ? [operationColumn] : [])];
        operationsArr.value = ['edit'];
    } else if (value === "task") {
        const operationColumn = getOperationColumn(['viewFile']);
        tableColumns.value = [...columns.value, ...(operationColumn ? [operationColumn] : [])];
        operationsArr.value = ['viewFile'];
    }
    pageNum.value = 1;
    pageSize.value = 10;
    getList();
  if (value === "taskManage") {
    const operationColumn = getOperationColumn(['edit']);
    tableColumns.value = [...columns.value, ...(operationColumn ? [operationColumn] : [])];
    operationsArr.value = ['edit'];
  } else if (value === "task") {
    const operationColumn = getOperationColumn(['viewFile']);
    tableColumns.value = [...columns.value, ...(operationColumn ? [operationColumn] : [])];
    operationsArr.value = ['viewFile'];
  }
  pageNum.value = 1;
  pageSize.value = 10;
  getList();
};
// æŸ¥è¯¢æ“ä½œ
const handleQuery = () => {
    pageNum.value = 1;
    pageSize.value = 10;
    getList();
  pageNum.value = 1;
  pageSize.value = 10;
  getList();
};
const pagination = (obj) => {
    pageNum.value = obj.page;
    pageSize.value = obj.limit;
// åˆ†é¡µå¤„理
const handlePagination = (val) => {
    pageNum.value = val.page;
    pageSize.value = val.limit;
    getList();
};
// èŽ·å–åˆ—è¡¨æ•°æ®
const getList = () => {
    tableLoading.value = true;
    const params = { ...queryParams, size: pageSize.value, current: pageNum.value };
    let apiCall;
    if (activeRadio.value === "task") {
        apiCall = inspectionTaskList(params);
    } else {
        apiCall = timingTaskList(params);
    }
    apiCall.then(res => {
        const rawData = res.data.records || [];
        // å¤„理 inspector å­—段,将字符串转换为数组(适用于所有情况)
        tableData.value = rawData.map(item => {
            const processedItem = { ...item };
            // å¤„理 inspector å­—段
            if (processedItem.inspector) {
                if (typeof processedItem.inspector === 'string') {
                    // å­—符串按逗号分割
                    processedItem.inspector = processedItem.inspector.split(',').map(s => s.trim()).filter(s => s);
                } else if (!Array.isArray(processedItem.inspector)) {
                    // éžæ•°ç»„转为数组
                    processedItem.inspector = [processedItem.inspector];
                }
            } else {
                // ç©ºå€¼è®¾ä¸ºç©ºæ•°ç»„
                processedItem.inspector = [];
            }
            return processedItem;
        });
        total.value = res.data.total || 0;
    }).finally(() => {
        tableLoading.value = false;
    });
  tableLoading.value = true;
  const params = { ...queryParams, size: pageSize.value, current: pageNum.value };
  let apiCall;
  if (activeRadio.value === "task") {
    apiCall = inspectionTaskList(params);
  } else {
    apiCall = timingTaskList(params);
  }
  apiCall.then(res => {
    const rawData = res.data.records || [];
    // å¤„理 inspector å­—段,将字符串转换为数组(适用于所有情况)
    tableData.value = rawData.map(item => {
      const processedItem = { ...item };
      // å¤„理 inspector å­—段
      if (processedItem.inspector) {
        if (typeof processedItem.inspector === 'string') {
          // å­—符串按逗号分割
          processedItem.inspector = processedItem.inspector.split(',').map(s => s.trim()).filter(s => s);
        } else if (!Array.isArray(processedItem.inspector)) {
          // éžæ•°ç»„转为数组
          processedItem.inspector = [processedItem.inspector];
        }
      } else {
        // ç©ºå€¼è®¾ä¸ºç©ºæ•°ç»„
        processedItem.inspector = [];
      }
      return processedItem;
    });
    total.value = res.data.total || 0;
  }).finally(() => {
    tableLoading.value = false;
  });
};
// é‡ç½®æŸ¥è¯¢
const resetQuery = () => {
    for (const key in queryParams) {
        if (!["pageNum", "pageSize"].includes(key)) {
            queryParams[key] = "";
        }
    }
    handleQuery();
  for (const key in queryParams) {
    if (!["pageNum", "pageSize"].includes(key)) {
      queryParams[key] = "";
    }
  }
  handleQuery();
};
// æ–°å¢ž / ç¼–辑
const handleAdd = (row) => {
    const type = row ? 'edit' : 'add';
    nextTick(() => {
        formDia.value?.openDialog(type, row);
    });
  const type = row ? 'edit' : 'add';
  nextTick(() => {
    formDia.value?.openDialog(type, row);
  });
};
// æŸ¥çœ‹é™„ä»¶
const viewFile = (row) => {
    nextTick(() => {
        viewFiles.value?.openDialog(row);
    });
  nextTick(() => {
    viewFiles.value?.openDialog(row);
  });
};
// åˆ é™¤æ“ä½œ
const handleDelete = () => {
    if (!selectedRows.value.length) {
        proxy.$modal.msgWarning("请选择要删除的数据");
        return;
    }
    const deleteIds = selectedRows.value.map(item => item.id);
    proxy.$modal.confirm('是否确认删除所选数据项?').then(() => {
        return delTimingTask(deleteIds);
    }).then(() => {
        proxy.$modal.msgSuccess("删除成功");
        handleQuery();
    }).catch(() => {});
  if (!selectedRows.value.length) {
    proxy.$modal.msgWarning("请选择要删除的数据");
    return;
  }
  const deleteIds = selectedRows.value.map(item => item.id);
  proxy.$modal.confirm('是否确认删除所选数据项?').then(() => {
    return delTimingTask(deleteIds);
  }).then(() => {
    proxy.$modal.msgSuccess("删除成功");
    handleQuery();
  }).catch(() => {});
};
// å¤šé€‰å˜æ›´
const handleSelectionChange = (selection) => {
    selectedRows.value = selection;
  selectedRows.value = selection;
};
// å¯¼å‡º
const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
    })
        .then(() => {
            // æ ¹æ®å½“前选中的标签页调用不同的导出接口
            if (activeRadio.value === "taskManage") {
                // å®šæ—¶ä»»åŠ¡ç®¡ç†
                proxy.download("/timingTask/export", {}, "定时任务管理.xlsx");
            } else if (activeRadio.value === "task") {
                // å®šæ—¶ä»»åŠ¡è®°å½•
                proxy.download("/inspectionTask/export", {}, "定时任务记录.xlsx");
            }
        })
        .catch(() => {
            proxy.$modal.msg("已取消");
        });
  ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      // æ ¹æ®å½“前选中的标签页调用不同的导出接口
      if (activeRadio.value === "taskManage") {
        // å®šæ—¶ä»»åŠ¡ç®¡ç†
        proxy.download("/timingTask/export", {}, "定时任务管理.xlsx");
      } else if (activeRadio.value === "task") {
        // å®šæ—¶ä»»åŠ¡è®°å½•
        proxy.download("/inspectionTask/export", {}, "定时任务记录.xlsx");
      }
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
};
</script>
<style scoped>
.person-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
.person-tag {
    margin-right: 4px;
    margin-bottom: 2px;
  margin-right: 4px;
  margin-bottom: 2px;
}
.no-data {
    color: #909399;
    font-size: 14px;
  color: #909399;
  font-size: 14px;
}
</style>
src/views/equipmentManagement/upkeep/index.vue
@@ -203,6 +203,13 @@
          >
            åˆ é™¤
          </el-button>
          <el-button
            type="primary"
            link
            @click="openFileDialog(row)"
          >
            é™„ä»¶
          </el-button>
        </template>
      </PIMTable>
        </div>
@@ -211,6 +218,15 @@
    <PlanModal ref="planModalRef" @ok="getTableData" />
        <MaintenanceModal ref="maintainModalRef" @ok="getTableData" />
        <FormDia ref="formDiaRef" @closeDia="getScheduledTableData" />
    <FileListDialog
      ref="fileListDialogRef"
      v-model="fileDialogVisible"
      :show-upload-button="true"
      :show-delete-button="true"
      :delete-method="handleAttachmentDelete"
      :name-column-label="'附件名称'"
      :rulesRegulationsManagementId="currentMaintenanceTaskId"
      @upload="handleAttachmentUpload" />
  </div>
</template>
@@ -221,12 +237,18 @@
import PlanModal from './Form/PlanModal.vue'
import MaintenanceModal from './Form/MaintenanceModal.vue'
import FormDia from './Form/formDia.vue'
import FileListDialog from '@/components/Dialog/FileListDialog.vue'
import {
  getUpkeepPage,
  delUpkeep,
  deviceMaintenanceTaskList,
  deviceMaintenanceTaskDel,
} from '@/api/equipmentManagement/upkeep'
import {
  listMaintenanceTaskFiles,
  addMaintenanceTaskFile,
  delMaintenanceTaskFile,
} from '@/api/equipmentManagement/maintenanceTaskFile'
import dayjs from 'dayjs'
const { proxy } = getCurrentInstance()
@@ -240,6 +262,10 @@
const maintainModalRef = ref()
// å®šæ—¶ä»»åŠ¡å¼¹çª—æŽ§åˆ¶å™¨
const formDiaRef = ref()
// é™„件弹窗
const fileListDialogRef = ref(null)
const fileDialogVisible = ref(false)
const currentMaintenanceTaskId = ref(null)
// ä»»åŠ¡è®°å½•tab(原设备保养页面)相关变量
const filters = reactive({
@@ -385,7 +411,7 @@
        dataType: "slot",
        slot: "operation",
        align: "center",
        width: "300px",
        width: "350px",
    },
])
@@ -571,6 +597,79 @@
  getTableData()
}
// é™„件相关方法
// æŸ¥è¯¢é™„件列表
const fetchMaintenanceTaskFiles = async (deviceMaintenanceId) => {
  try {
    const params = {
      current: 1,
      size: 100,
      deviceMaintenanceId,
      rulesRegulationsManagementId:deviceMaintenanceId
    }
    const res = await listMaintenanceTaskFiles(params)
    const records = res?.data?.records || []
    const mapped = records.map(item => ({
      id: item.id,
      name: item.fileName || item.name,
      url: item.fileUrl || item.url,
      raw: item,
    }))
    fileListDialogRef.value?.setList(mapped)
  } catch (error) {
    ElMessage.error('获取附件列表失败')
  }
}
// æ‰“开附件弹窗
const openFileDialog = async (row) => {
  currentMaintenanceTaskId.value = row.id
  fileDialogVisible.value = true
  await fetchMaintenanceTaskFiles(row.id)
}
// åˆ·æ–°é™„件列表
const refreshFileList = async () => {
  if (!currentMaintenanceTaskId.value) return
  await fetchMaintenanceTaskFiles(currentMaintenanceTaskId.value)
}
// ä¸Šä¼ é™„ä»¶
const handleAttachmentUpload = async (filePayload) => {
  if (!currentMaintenanceTaskId.value) return
  try {
    const payload = {
      name: filePayload?.fileName || filePayload?.name,
      url: filePayload?.fileUrl || filePayload?.url,
      deviceMaintenanceId: currentMaintenanceTaskId.value,
    }
    await addMaintenanceTaskFile(payload)
    ElMessage.success('文件上传成功')
    await refreshFileList()
  } catch (error) {
    ElMessage.error('文件上传失败')
  }
}
// åˆ é™¤é™„ä»¶
const handleAttachmentDelete = async (row) => {
  if (!row?.id) return false
  try {
    await ElMessageBox.confirm('确认删除该附件?', '提示', { type: 'warning' })
  } catch {
    return false
  }
  try {
    await delMaintenanceTaskFile(row.id)
    ElMessage.success('删除成功')
    await refreshFileList()
    return true
  } catch (error) {
    ElMessage.error('删除失败')
    return false
  }
}
onMounted(() => {
  // æ ¹æ®é»˜è®¤æ¿€æ´»çš„ Tab è°ƒç”¨å¯¹åº”的查询接口
  if (activeTab.value === 'scheduled') {
src/views/inventoryManagement/stockManagement/FrozenAndThaw.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,164 @@
<template>
  <div>
    <el-dialog
        v-model="isShow"
        :title="operationType === 'frozen' ? '冻结库存' : '解冻库存'"
        width="800"
        @close="closeModal"
    >
      <el-form label-width="140px" :model="formState" ref="formRef">
        <el-form-item
            :label="operationType === 'frozen' ? '冻结数量:' : '解冻数量:'"
            prop="lockedQuantity"
        >
          <el-input-number v-model="formState.lockedQuantity" :step="1" :min="1" precision="0" style="width: 100%" :max="maxCount" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="handleSubmit">确认</el-button>
          <el-button @click="closeModal">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {ref, computed, getCurrentInstance} from "vue";
import {frozenStockInventory, thawStockInventory} from "@/api/inventoryManagement/stockInventory.js";
import {frozenStockUninventory, thawStockUninventory} from "@/api/inventoryManagement/stockUninventory.js";
const props = defineProps({
  visible: {
    type: Boolean,
    required: true,
  },
  operationType: {
    type: String,
    required: true,
    default: 'frozen',
  },
  type: {
    type: String,
    required: true,
    default: 'qualified',
  },
  record: {
    type: Object,
    default: () => {},
  }
});
const emit = defineEmits(['update:visible', 'completed']);
// å“åº”式数据(替代选项式的 data)
const formState = ref({
  lockedQuantity: 0,
});
const isShow = computed({
  get() {
    return props.visible;
  },
  set(val) {
    emit('update:visible', val);
  },
});
let { proxy } = getCurrentInstance()
const closeModal = () => {
  // é‡ç½®è¡¨å•数据
  formState.value = {
    lockedQuantity: undefined
  };
  isShow.value = false;
};
const maxCount = computed(() => {
  // å†»ç»“库存最大数量为未解冻数量
  if (props.operationType === 'frozen') {
    return props.record.unLockedQuantity
  }
  // è§£å†»åº“存最大数量为已冻结数量
  return props.record.lockedQuantity
})
const handleSubmit = () => {
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      const data = Object.assign({id: props.record.id}, formState.value);
      if (props.type === 'qualified') {
        // å†»ç»“
        if (props.operationType === 'frozen') {
          frozenStockInventory(data).then(res => {
            if (res.code === 200) {
              // å…³é—­æ¨¡æ€æ¡†
              isShow.value = false;
              // å‘ŠçŸ¥çˆ¶ç»„件已完成
              emit('completed');
              proxy.$modal.msgSuccess("提交成功");
            } else {
              proxy.$modal.msgError(res.msg);
            }
          })
        } else {
          thawStockInventory(data).then(res => {
            if (res.code === 200) {
              // å…³é—­æ¨¡æ€æ¡†
              isShow.value = false;
              // å‘ŠçŸ¥çˆ¶ç»„件已完成
              emit('completed');
              proxy.$modal.msgSuccess("提交成功");
            } else {
              proxy.$modal.msgError(res.msg);
            }
          })
        }
      } else {
        if (props.operationType === 'frozen') {
          frozenStockUninventory(data).then(res => {
            if (res.code === 200) {
              // å…³é—­æ¨¡æ€æ¡†
              isShow.value = false;
              // å‘ŠçŸ¥çˆ¶ç»„件已完成
              emit('completed');
              proxy.$modal.msgSuccess("提交成功");
            } else {
              proxy.$modal.msgError(res.msg);
            }
          })
        } else {
          thawStockUninventory(data).then(res => {
            if (res.code === 200) {
              // å…³é—­æ¨¡æ€æ¡†
              isShow.value = false;
              // å‘ŠçŸ¥çˆ¶ç»„件已完成
              emit('completed');
              proxy.$modal.msgSuccess("提交成功");
            } else {
              proxy.$modal.msgError(res.msg);
            }
          })
        }
      }
    }
  })
};
onMounted(() => {
  formState.value.lockedQuantity = maxCount.value;
})
defineExpose({
  closeModal,
  handleSubmit,
  isShow,
});
</script>
src/views/inventoryManagement/stockManagement/Import.vue
@@ -6,8 +6,9 @@
      :headers="upload.headers"
      :action="upload.url"
      :disabled="upload.isUploading"
      :showTip="false"
      :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/New.vue
@@ -38,10 +38,18 @@
        </el-form-item>
        <el-form-item
            label="数量"
            label="库存数量"
            prop="qualitity"
        >
          <el-input-number v-model="formState.qualitity" :step="1" :min="0" style="width: 100%" />
          <el-input-number v-model="formState.qualitity" :step="1" :min="1" style="width: 100%" />
        </el-form-item>
        <el-form-item
            v-if="type === 'qualified'"
            label="库存预警数量"
            prop="warnNum"
        >
          <el-input-number v-model="formState.warnNum" :step="1" :min="0" :max="formState.qualitity" style="width: 100%" />
        </el-form-item>
        <el-form-item label="备注" prop="remark">
@@ -94,6 +102,7 @@
  productModelName: "",
  unit: "",
  qualitity: 0,
  warnNum: 0,
  remark: '',
});
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>
@@ -28,12 +27,15 @@
        <el-table-column label="规格型号" prop="model" show-overflow-tooltip />
        <el-table-column label="单位" prop="unit" show-overflow-tooltip />
        <el-table-column label="库存数量" prop="qualitity" show-overflow-tooltip />
        <el-table-column label="冻结数量" prop="lockedQuantity" show-overflow-tooltip />
        <el-table-column label="库存预警数量" prop="warnNum"  show-overflow-tooltip />
        <el-table-column label="备注" prop="remark"  show-overflow-tooltip />
        <el-table-column label="最近更新时间" prop="updateTime" show-overflow-tooltip />
        <el-table-column fixed="right" label="操作" min-width="60" align="center">
          <template #default="scope">
            <el-button link type="primary" size="small" @click="showSubtractModal(scope.row)" :disabled="scope.row.qualitity === 0">领用</el-button>
            <el-button link type="primary" size="small" @click="showSubtractModal(scope.row)" :disabled="scope.row.unLockedQuantity === 0">领用</el-button>
            <el-button link type="primary" size="small" v-if="scope.row.unLockedQuantity > 0" @click="showFrozenModal(scope.row)">冻结</el-button>
            <el-button link type="primary" size="small" v-if="scope.row.lockedQuantity > 0" @click="showThawModal(scope.row)">解冻</el-button>
          </template>
        </el-table-column>
      </el-table>
@@ -48,12 +50,20 @@
    <subtract-stock-inventory v-if="isShowSubtractModal"
                 v-model:visible="isShowSubtractModal"
                 :record="record"
                 type="qualified"
                 @completed="handleQuery" />
    <!-- å¯¼å…¥åº“å­˜-->
    <import-stock-inventory v-if="isShowImportModal"
                 v-model:visible="isShowImportModal"
                 type="qualified"
                 @uploadSuccess="handleQuery" />
    <!-- å†»ç»“/解冻库存-->
    <frozen-and-thaw-stock-inventory v-if="isShowFrozenAndThawModal"
                 v-model:visible="isShowFrozenAndThawModal"
                 :record="record"
                 :operation-type="operationType"
                 type="qualified"
                 @completed="handleQuery" />
  </div>
</template>
@@ -65,6 +75,7 @@
const NewStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/New.vue"));
const SubtractStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/Subtract.vue"));
const ImportStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/Import.vue"));
const FrozenAndThawStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/FrozenAndThaw.vue"));
const { proxy } = getCurrentInstance()
const tableData = ref([])
const selectedRows = ref([])
@@ -79,6 +90,10 @@
const isShowNewModal = ref(false)
// æ˜¯å¦æ˜¾ç¤ºé¢†ç”¨å¼¹æ¡†
const isShowSubtractModal = ref(false)
// æ˜¯å¦æ˜¾ç¤ºå†»ç»“/解冻弹框
const isShowFrozenAndThawModal = ref(false)
// æ“ä½œç±»åž‹
const operationType = ref('frozen')
// æ˜¯å¦æ˜¾ç¤ºå¯¼å…¥å¼¹æ¡†
const isShowImportModal = ref(false)
const data = reactive({
@@ -129,6 +144,20 @@
  isShowSubtractModal.value = true
}
// ç‚¹å‡»å†»ç»“
const showFrozenModal = (row) => {
  record.value = row
  isShowFrozenAndThawModal.value = true
  operationType.value = 'frozen'
}
// ç‚¹å‡»è§£å†»
const showThawModal = (row) => {
  record.value = row
  isShowFrozenAndThawModal.value = true
  operationType.value = 'thaw'
}
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  // è¿‡æ»¤æŽ‰å­æ•°æ®
@@ -139,7 +168,7 @@
// è¡¨æ ¼è¡Œç±»å
const tableRowClassName = ({ row }) => {
  const stock = Number(row?.inboundNum0 ?? 0);
  const stock = Number(row?.unLockedQuantity ?? 0);
  const warn = Number(row?.warnNum ?? 0);
  if (!Number.isFinite(stock) || !Number.isFinite(warn)) {
    return '';
@@ -161,10 +190,6 @@
  }).catch(() => {
    proxy.$modal.msg("已取消")
  })
}
const importTemplate =() =>{
  proxy.download("/stockInventory/downloadStockInventory", {}, "库存导入模板.xlsx");
}
onMounted(() => {
src/views/inventoryManagement/stockManagement/Subtract.vue
@@ -94,7 +94,7 @@
})
const maxQuality = computed(() => {
  return props.record.qualitity ? props.record.qualitity :  0;
  return props.record.unLockedQuantity ? props.record.unLockedQuantity :  0;
})
const initFormData = () => {
src/views/inventoryManagement/stockManagement/Unqualified.vue
@@ -24,12 +24,14 @@
        <el-table-column label="规格型号" prop="model" show-overflow-tooltip />
        <el-table-column label="单位" prop="unit" show-overflow-tooltip />
        <el-table-column label="库存数量" prop="qualitity" show-overflow-tooltip />
        <el-table-column label="库存预警数量" prop="warnNum"  show-overflow-tooltip />
        <el-table-column label="冻结数量" prop="lockedQuantity" show-overflow-tooltip />
        <el-table-column label="备注" prop="remark"  show-overflow-tooltip />
        <el-table-column label="最近更新时间" prop="updateTime" show-overflow-tooltip />
        <el-table-column fixed="right" label="操作" min-width="60" align="center">
          <template #default="scope">
            <el-button link type="primary" size="small" @click="showSubtractModal(scope.row)" :disabled="scope.row.qualitity === 0">领用</el-button>
            <el-button link type="primary" size="small" @click="showSubtractModal(scope.row)" :disabled="scope.row.unLockedQuantity === 0">领用</el-button>
            <el-button link type="primary" size="small" v-if="scope.row.unLockedQuantity > 0" @click="showFrozenModal(scope.row)">冻结</el-button>
            <el-button link type="primary" size="small" v-if="scope.row.lockedQuantity > 0" @click="showThawModal(scope.row)">解冻</el-button>
          </template>
        </el-table-column>
      </el-table>
@@ -44,7 +46,15 @@
    <subtract-stock-inventory v-if="isShowSubtractModal"
                 v-model:visible="isShowSubtractModal"
                 :record="record"
                 type="unqualified"
                 @completed="handleQuery" />
    <!-- å†»ç»“/解冻库存-->
    <frozen-and-thaw-stock-inventory v-if="isShowFrozenAndThawModal"
                                     v-model:visible="isShowFrozenAndThawModal"
                                     :record="record"
                                     :operation-type="operationType"
                                     type="unqualified"
                                     @completed="handleQuery" />
  </div>
</template>
@@ -55,6 +65,7 @@
import { getStockUninventoryListPage } from "@/api/inventoryManagement/stockUninventory.js";
const NewStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/New.vue"));
const SubtractStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/Subtract.vue"));
const FrozenAndThawStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/FrozenAndThaw.vue"));
const { proxy } = getCurrentInstance()
const tableData = ref([])
@@ -70,6 +81,10 @@
const isShowNewModal = ref(false)
// æ˜¯å¦æ˜¾ç¤ºé¢†ç”¨å¼¹æ¡†
const isShowSubtractModal = ref(false)
// æ˜¯å¦æ˜¾ç¤ºå†»ç»“/解冻弹框
const isShowFrozenAndThawModal = ref(false)
// æ“ä½œç±»åž‹
const operationType = ref('frozen')
const data = reactive({
  searchForm: {
    productName: '',
@@ -107,6 +122,20 @@
  isShowSubtractModal.value = true
}
// ç‚¹å‡»å†»ç»“
const showFrozenModal = (row) => {
  record.value = row
  isShowFrozenAndThawModal.value = true
  operationType.value = 'frozen'
}
// ç‚¹å‡»è§£å†»
const showThawModal = (row) => {
  record.value = row
  isShowFrozenAndThawModal.value = true
  operationType.value = 'thaw'
}
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  // è¿‡æ»¤æŽ‰å­æ•°æ®
@@ -117,12 +146,12 @@
// è¡¨æ ¼è¡Œç±»å
const tableRowClassName = ({ row }) => {
  const stock = Number(row?.inboundNum0 ?? 0);
  const warn = Number(row?.warnNum ?? 0);
  if (!Number.isFinite(stock) || !Number.isFinite(warn)) {
    return '';
  }
  return stock < warn ? 'row-low-stock' : '';
  // const stock = Number(row?.unLockedQuantity ?? 0);
  // const warn = Number(row?.warnNum ?? 0);
  // if (!Number.isFinite(stock) || !Number.isFinite(warn)) {
  //   return '';
  // }
  // return stock < warn ? 'row-low-stock' : '';
};
// å¯¼å‡º
src/views/inventoryManagement/stockReport/index.vue
@@ -12,7 +12,6 @@
        >
          <el-option label="日报" value="daily" />
          <el-option label="月报" value="monthly" />
          <el-option label="作业报表" value="work" />
          <el-option label="进出存报表" value="inout" />
        </el-select>
        
@@ -54,93 +53,93 @@
        </el-button>
        <el-button @click="handleReset">重置</el-button>
      </div>
      <div class="search_right">
        <el-button type="success" @click="handleExport" icon="Download">
          å¯¼å‡ºæŠ¥è¡¨
        </el-button>
<!--        <el-button type="success" @click="handleExport" icon="Download">-->
<!--          å¯¼å‡ºæŠ¥è¡¨-->
<!--        </el-button>-->
      </div>
    </div>
    <!-- ç»Ÿè®¡å¡ç‰‡ -->
    <div class="stats_cards" v-if="reportData.summary">
      <el-row :gutter="20">
        <el-col :span="6">
          <el-card class="stats_card">
            <div class="stats_content">
              <div class="stats_icon in">
                <el-icon><TrendCharts /></el-icon>
              </div>
              <div class="stats_info">
                <div class="stats_value">{{ reportData.summary.totalIn || 0 }}</div>
                <div class="stats_label">总入库量</div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card class="stats_card">
            <div class="stats_content">
              <div class="stats_icon out">
                <el-icon><TrendCharts /></el-icon>
              </div>
              <div class="stats_info">
                <div class="stats_value">{{ reportData.summary.totalOut || 0 }}</div>
                <div class="stats_label">总出库量</div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card class="stats_card">
            <div class="stats_content">
              <div class="stats_icon stock">
                <el-icon><Box /></el-icon>
              </div>
              <div class="stats_info">
                <div class="stats_value">{{ reportData.summary.currentStock || 0 }}</div>
                <div class="stats_label">当前库存</div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="6">
          <el-card class="stats_card">
            <div class="stats_content">
              <div class="stats_icon turnover">
                <el-icon><Refresh /></el-icon>
              </div>
              <div class="stats_info">
                <div class="stats_value">{{ reportData.summary.turnoverRate || 0 }}%</div>
                <div class="stats_label">周转率</div>
              </div>
            </div>
          </el-card>
        </el-col>
      </el-row>
    </div>
<!--    &lt;!&ndash; ç»Ÿè®¡å¡ç‰‡ &ndash;&gt;-->
<!--    <div class="stats_cards" v-if="reportData.summary">-->
<!--      <el-row :gutter="20">-->
<!--        <el-col :span="6">-->
<!--          <el-card class="stats_card">-->
<!--            <div class="stats_content">-->
<!--              <div class="stats_icon in">-->
<!--                <el-icon><TrendCharts /></el-icon>-->
<!--              </div>-->
<!--              <div class="stats_info">-->
<!--                <div class="stats_value">{{ reportData.summary.totalIn || 0 }}</div>-->
<!--                <div class="stats_label">总入库量</div>-->
<!--              </div>-->
<!--            </div>-->
<!--          </el-card>-->
<!--        </el-col>-->
<!--        <el-col :span="6">-->
<!--          <el-card class="stats_card">-->
<!--            <div class="stats_content">-->
<!--              <div class="stats_icon out">-->
<!--                <el-icon><TrendCharts /></el-icon>-->
<!--              </div>-->
<!--              <div class="stats_info">-->
<!--                <div class="stats_value">{{ reportData.summary.totalOut || 0 }}</div>-->
<!--                <div class="stats_label">总出库量</div>-->
<!--              </div>-->
<!--            </div>-->
<!--          </el-card>-->
<!--        </el-col>-->
<!--        <el-col :span="6">-->
<!--          <el-card class="stats_card">-->
<!--            <div class="stats_content">-->
<!--              <div class="stats_icon stock">-->
<!--                <el-icon><Box /></el-icon>-->
<!--              </div>-->
<!--              <div class="stats_info">-->
<!--                <div class="stats_value">{{ reportData.summary.currentStock || 0 }}</div>-->
<!--                <div class="stats_label">当前库存</div>-->
<!--              </div>-->
<!--            </div>-->
<!--          </el-card>-->
<!--        </el-col>-->
<!--        <el-col :span="6">-->
<!--          <el-card class="stats_card">-->
<!--            <div class="stats_content">-->
<!--              <div class="stats_icon turnover">-->
<!--                <el-icon><Refresh /></el-icon>-->
<!--              </div>-->
<!--              <div class="stats_info">-->
<!--                <div class="stats_value">{{ reportData.summary.turnoverRate || 0 }}%</div>-->
<!--                <div class="stats_label">周转率</div>-->
<!--              </div>-->
<!--            </div>-->
<!--          </el-card>-->
<!--        </el-col>-->
<!--      </el-row>-->
<!--    </div>-->
    <!-- å›¾è¡¨åŒºåŸŸ -->
    <div class="chart_section" v-if="reportData.chartData">
      <el-row :gutter="20">
        <el-col :span="12">
          <el-card>
            <template #header>
              <span>库存趋势图</span>
            </template>
            <div ref="trendChart" style="height: 300px;"></div>
          </el-card>
        </el-col>
        <el-col :span="12">
          <el-card>
            <template #header>
              <span>进出库对比</span>
            </template>
            <div ref="comparisonChart" style="height: 300px;"></div>
          </el-card>
        </el-col>
      </el-row>
    </div>
<!--    &lt;!&ndash; å›¾è¡¨åŒºåŸŸ &ndash;&gt;-->
<!--    <div class="chart_section" v-if="reportData.chartData">-->
<!--      <el-row :gutter="20">-->
<!--        <el-col :span="12">-->
<!--          <el-card>-->
<!--            <template #header>-->
<!--              <span>库存趋势图</span>-->
<!--            </template>-->
<!--            <div ref="trendChart" style="height: 300px;"></div>-->
<!--          </el-card>-->
<!--        </el-col>-->
<!--        <el-col :span="12">-->
<!--          <el-card>-->
<!--            <template #header>-->
<!--              <span>进出库对比</span>-->
<!--            </template>-->
<!--            <div ref="comparisonChart" style="height: 300px;"></div>-->
<!--          </el-card>-->
<!--        </el-col>-->
<!--      </el-row>-->
<!--    </div>-->
    <!-- è¯¦ç»†æ•°æ®è¡¨æ ¼ -->
    <div class="table_section">
@@ -163,122 +162,72 @@
            width="60"
          />
           <el-table-column
             v-if="searchForm.reportType === 'daily'"
             label="日期"
             prop="createTime"
             width="100"
             align="center"
           />
           <el-table-column
             v-if="searchForm.reportType === 'monthly'"
             label="月份"
             prop="createTime"
             width="100"
             align="center"
           />
           <el-table-column
             label="入库时间"
             prop="createTime"
             width="100"
             width="200"
             show-overflow-tooltip
             v-if="searchForm.reportType !== 'inout'"
           />
           <el-table-column
             label="入库批次"
             prop="inboundBatches"
             width="160"
             width="240"
             show-overflow-tooltip
           />
           <el-table-column
             label="供应商名称"
             prop="supplierName"
             min-width="240"
             show-overflow-tooltip
             v-if="searchForm.reportType !== 'inout'"
           />
           <el-table-column
             label="产品大类"
             prop="productCategory"
             width="100"
             prop="productName"
             show-overflow-tooltip
           />
           <el-table-column
             label="规格型号"
             prop="specificationModel"
             min-width="200"
             prop="model"
             show-overflow-tooltip
           />
           <el-table-column
             label="单位"
             prop="unit"
             width="70"
             show-overflow-tooltip
           />
           <!-- <el-table-column
             label="期初库存"
             prop="beginStock"
             width="100"
             align="center"
           /> -->
           <el-table-column
             label="入库数量"
             prop="inboundNum"
             width="100"
             prop="totalStockIn"
             align="center"
             v-if="searchForm.reportType === 'inout'"
           />
           <!-- <el-table-column
           <el-table-column
               label="入库数量"
               prop="stockInNum"
               align="center"
               v-else
           />
           <el-table-column
             label="出库数量"
             prop=""
             prop="totalStockOut"
             width="100"
             align="center"
           /> -->
             v-if="searchForm.reportType === 'inout'"
           />
           <el-table-column
             label="现在库存"
             prop="inboundNum0"
             width="100"
             prop="currentStock"
             align="center"
           />
           <el-table-column
             label="含税单价"
             prop="taxInclusiveUnitPrice"
             width="100"
             show-overflow-tooltip
           />
           <el-table-column
             label="含税总价"
             prop="taxInclusiveTotalPrice"
             width="100"
             show-overflow-tooltip
           />
           <el-table-column
             label="税率(%)"
             prop="taxRate"
             width="80"
             show-overflow-tooltip
           />
           <el-table-column
             label="不含税总价"
             prop="taxExclusiveTotalPrice"
             width="100"
             show-overflow-tooltip
           />
           <el-table-column label="来源"
                            prop="recordType"
                            v-if="searchForm.reportType !== 'inout'"
                            show-overflow-tooltip>
             <template #default="scope">
               {{ getRecordType(scope.row.recordType) }}
             </template>
           </el-table-column>
           <el-table-column
             label="入库人"
             prop="createBy"
             width="80"
             v-if="searchForm.reportType !== 'inout'"
             show-overflow-tooltip
           />
           <el-table-column
             v-if="searchForm.reportType === 'work'"
             label="操作人员"
             prop="operator"
             width="80"
             align="center"
           />
           <el-table-column
             v-if="searchForm.reportType === 'work'"
             label="操作时间"
             prop="operateTime"
             width="150"
             align="center"
           />
        </el-table>
      </el-card>
@@ -291,12 +240,14 @@
import { ElMessage } from 'element-plus'
import * as echarts from 'echarts'
import {
  getStockDailyReport,
  getStockMonthlyReport,
  getWorkReport,
  getStockInOutReport,
  exportStockReport
} from '@/api/inventoryManagement/stockReport'
import {
  getStockInventoryInAndOutReportList,
  getStockInventoryReportList
} from "@/api/inventoryManagement/stockInventory.js";
import {findAllQualifiedStockRecordTypeOptions} from "@/api/basicData/enum.js";
const { proxy } = getCurrentInstance()
@@ -318,12 +269,25 @@
  tableData: []
})
const stockRecordTypeOptions = ref([])
const getRecordType = (recordType) => {
  return stockRecordTypeOptions.value.find(item => item.value === recordType)?.label || ''
}
// èŽ·å–æ¥æºç±»åž‹é€‰é¡¹
const fetchStockRecordTypeOptions = () => {
  findAllQualifiedStockRecordTypeOptions()
      .then(res => {
        stockRecordTypeOptions.value = res.data;
      })
}
// èŽ·å–è¡¨æ ¼æ ‡é¢˜
const getTableTitle = () => {
  const typeMap = {
    daily: '日报详细数据',
    monthly: '月报详细数据',
    work: '作业报表详细数据',
    inout: '进出存报表详细数据'
  }
  return typeMap[searchForm.reportType] || '报表详细数据'
@@ -348,32 +312,19 @@
  try {
    const params = getQueryParams()
    let response
    switch (searchForm.reportType) {
      case 'daily':
        response = await getStockDailyReport(params)
        break
      case 'monthly':
        response = await getStockMonthlyReport(params)
        break
      case 'work':
        response = await getWorkReport(params)
        break
      case 'inout':
        response = await getStockInOutReport(params)
        break
      default:
        throw new Error('未知的报表类型')
    if (searchForm.reportType === 'inout') {
      response = await getStockInventoryInAndOutReportList(params)
    } else {
      response = await getStockInventoryReportList(params)
    }
    if (response.code === 200) {
      // generateMockData()
      reportData.value.tableData = response.data.tableData
      reportData.value.summary = response.data.summary
      reportData.value.chartData = response.data.chartData
      nextTick(() => {
        initCharts()
      })
      reportData.value.tableData = response.data.records
      // reportData.value.summary = response.data.summary
      // reportData.value.chartData = response.data.chartData
      // nextTick(() => {
      //   initCharts()
      // })
      
    }
  } catch (error) {
@@ -420,7 +371,7 @@
      ElMessage.warning('请选择日期')
      return false
    }
  } else if (searchForm.reportType === 'work' || searchForm.reportType === 'inout') {
  } else if (searchForm.reportType === 'inout') {
    if (!searchForm.dateRange || searchForm.dateRange.length !== 2) {
      ElMessage.warning('请选择日期范围')
      return false
@@ -599,6 +550,8 @@
    yesterday.toISOString().split('T')[0],
    today.toISOString().split('T')[0]
  ]
  fetchStockRecordTypeOptions()
})
</script>
src/views/procurementManagement/paymentLedger/index.vue
@@ -238,7 +238,7 @@
const getPaymenRecordtList = (supplierId) => {
  tableLoadingSon.value = true;
  paymentRecordList(supplierId)
  paymentRecordList({supplierId: supplierId})
    .then((res) => {
      tableLoadingSon.value = false;
      tableDataSon.value = res.data;
src/views/reportAnalysis/dataDashboard/components/DateTypeSwitch.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,94 @@
<template>
  <el-radio-group
    v-model="currentValue"
    class="date-type-switch"
    @change="handleChange"
  >
    <el-radio-button :label="1">周</el-radio-button>
    <el-radio-button :label="2">月</el-radio-button>
    <el-radio-button :label="3">季度</el-radio-button>
  </el-radio-group>
</template>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
  modelValue: {
    type: Number,
    default: 1, // é»˜è®¤é€‰ä¸­"周"
  },
})
const emit = defineEmits(['update:modelValue', 'change'])
const currentValue = ref(props.modelValue)
// ç›‘听外部值变化
watch(
  () => props.modelValue,
  (newVal) => {
    currentValue.value = newVal
  }
)
// å¤„理值变化
const handleChange = (value) => {
  emit('update:modelValue', value)
  emit('change', value)
}
</script>
<style scoped>
.date-type-switch {
  display: inline-flex;
}
/* æœªé€‰ä¸­çŠ¶æ€çš„æ ·å¼ */
.date-type-switch :deep(.el-radio-button__inner) {
  background-color: rgba(26, 88, 176, 0.3);
  color: rgba(184, 200, 224, 0.8);
  border-color: rgba(255, 255, 255, 0.2);
  border-radius: 0;
  padding: 6px 20px;
  font-size: 14px;
  transition: all 0.3s;
}
/* ç¬¬ä¸€ä¸ªæŒ‰é’®å·¦ä¾§åœ†è§’ */
.date-type-switch :deep(.el-radio-button:first-child .el-radio-button__inner) {
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;
}
/* æœ€åŽä¸€ä¸ªæŒ‰é’®å³ä¾§åœ†è§’ */
.date-type-switch :deep(.el-radio-button:last-child .el-radio-button__inner) {
  border-top-right-radius: 4px;
  border-bottom-right-radius: 4px;
}
/* æŒ‰é’®ä¹‹é—´çš„分隔线 */
.date-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
  border-right: 1px solid rgba(255, 255, 255, 0.2);
}
/* é€‰ä¸­çŠ¶æ€çš„æ ·å¼ */
.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
  background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
  color: #ffffff;
  border-color: rgba(51, 120, 255, 0.8);
  box-shadow: none;
}
/* æ‚¬åœæ•ˆæžœ */
.date-type-switch :deep(.el-radio-button__inner:hover) {
  color: rgba(184, 200, 224, 1);
  border-color: rgba(255, 255, 255, 0.3);
}
/* é€‰ä¸­çŠ¶æ€æ‚¬åœ */
.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
  background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
  color: #ffffff;
}
</style>
src/views/reportAnalysis/dataDashboard/components/PanelHeader.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
<template>
  <div class="panel-header">
    <span class="panel-title">{{ title }}</span>
  </div>
</template>
<script setup>
defineProps({
  title: {
    type: String,
    required: true,
    default: ''
  }
})
</script>
<style scoped>
.panel-header {
  background-image: url("@/assets/BI/kehuhetongback@2x.png");
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
}
.panel-title {
  width: 100%;
  font-weight: 500;
  font-size: 16px;
  color: #D9ECFF;
  padding-left: 46px;
  line-height: 36px;
}
</style>
src/views/reportAnalysis/dataDashboard/components/basic/center-bottom.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,165 @@
<template>
  <div>
    <PanelHeader title="人员分布" />
    <div class="main-panel panel-item-customers">
      <Echarts
        ref="echartsRef"
        :chartStyle="chartStyle"
        :legend="pieLegend"
        :series="pieSeries"
        :tooltip="pieTooltip"
        :color="pieColors"
        :options="pieOptions"
        style="height: 320px"
      />
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { deptStaffDistribution } from '@/api/viewIndex.js'
import PanelHeader from '../PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
/**
 * @introduction æŠŠæ•°ç»„中key值相同的那一项提取出来,组成一个对象
 * @param {参数类型} array ä¼ å…¥çš„æ•°ç»„ [{a:"1",b:"2"},{a:"2",b:"3"}]
 * @param {参数类型} key  å±žæ€§å a
 * @return {返回类型说明}
 */
function array2obj(array, key) {
  const resObj = {}
  for (let i = 0; i < array.length; i++) {
    resObj[array[i][key]] = array[i]
  }
  return resObj
}
const chartStyle = {
  width: '100%',
  height: '100%',
}
const echartsRef = ref(null)
const pieDatas = ref([])
const pieColors = ['#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF', '#43e8fc', '#27EBE7']
const pieObjData = computed(() => array2obj(pieDatas.value, 'name'))
const pieLegend = computed(() => {
  const data = pieDatas.value.map((d, idx) => ({
    name: d.name,
    icon: 'circle',
    textStyle: {
      fontSize: 18,
      color: pieColors[idx % pieColors.length],
    },
  }))
  return {
    orient: 'vertical',
    top: 'center',
    left: '50%',
    itemGap: 30,
    data: data,
    formatter: function (name) {
      const item = pieObjData.value[name]
      if (!item) return name
      return `{title|${name}}{value|${item.value}}{unit|人}{percent|${item.rate}}{unit|%}`
    },
    textStyle: {
      rich: {
        value: {
          color: '#43e8fc',
          fontSize: 18,
          fontWeight: 600,
          padding: [0, 10, 0, 30],
        },
        unit: {
          color: '#82baff',
          fontSize: 14,
          fontWeight: 600,
          padding: [0, 50, 0, 0],
        },
        percent: {
          color: '#43e8fc',
          fontSize: 18,
          fontWeight: 600,
          padding: [0, 10, 0, 0],
        },
        title: {
          fontSize: 18,
          padding: [0, 0, 0, 0],
        },
      },
    },
  }
})
const pieTooltip = {
  trigger: 'item',
  formatter: '{a} <br/>{b} : {c} ({d}%)',
}
const pieSeries = computed(() => [
  {
    name: '人员分布',
    type: 'pie',
    radius: '70%',
    center: ['20%', '50%'],
    itemStyle: {
      borderColor: '#0a1c3a',
      borderWidth: 2,
    },
    label: {
      show: false
    },
    minAngle: 15,
    data: pieDatas.value,
    animationType: 'scale',
    animationEasing: 'elasticOut',
    animationDelay: function () {
      return Math.random() * 200
    },
  },
])
const pieOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: '#B8C8E0' },
}
const getDeptStaffDistribution = () => {
  deptStaffDistribution().then(res => {
    if (res.code === 200) {
      pieDatas.value = res.data.items.map(item => ({
        name: item.name,
        value: parseInt(item.value),
        rate: item.rate
      }))
    }
  }).catch(err => {
    console.error('获取部门人员分布数据失败:', err)
  })
}
onMounted(() => {
  getDeptStaffDistribution()
})
</script>
<style scoped>
.main-panel {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 370px;
}
</style>
src/views/reportAnalysis/dataDashboard/components/basic/center-top.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,519 @@
<template>
  <div>
    <!-- é¡¶éƒ¨ç»Ÿè®¡å¡ç‰‡ -->
    <div class="stats-cards">
      <div class="stat-card">
        <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        <div class="card-content">
          <span class="card-label">员工总数</span>
          <span class="card-value">{{ totalStaff }}</span>
          <div class="card-compare" :class="compareClass(staffYoY)">
            <span>同比</span>
            <span class="compare-value">{{ formatPercent(staffYoY) }}</span>
            <span class="compare-icon">{{ staffYoY >= 0 ? '↑' : '↓' }}</span>
          </div>
        </div>
      </div>
      <div class="stat-card">
        <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        <div class="card-content">
          <span class="card-label">客户总数</span>
          <span class="card-value">{{ totalCustomers }}</span>
          <div class="card-compare" :class="compareClass(customersYoY)">
            <span>同比</span>
            <span class="compare-value">{{ formatPercent(customersYoY) }}</span>
            <span class="compare-icon">{{ customersYoY >= 0 ? '↑' : '↓' }}</span>
          </div>
        </div>
      </div>
      <div class="stat-card">
        <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        <div class="card-content">
          <span class="card-label">供应商总数</span>
          <span class="card-value">{{ totalSuppliers }}</span>
          <div class="card-compare" :class="compareClass(suppliersYoY)">
            <span>同比</span>
            <span class="compare-value">{{ formatPercent(suppliersYoY) }}</span>
            <span class="compare-icon">{{ suppliersYoY >= 0 ? '↑' : '↓' }}</span>
          </div>
        </div>
      </div>
    </div>
    <!-- è®¾å¤‡ç»Ÿè®¡ -->
    <div class="equipment-stats">
      <div class="equipment-header">
        <img
          src="@/assets/BI/shujutongjiicon@2x.png"
          alt="图标"
          class="equipment-icon"
        />
        <span class="equipment-title">设备统计</span>
      </div>
      <div class="equipment-items">
        <div class="equipment-item">
          <span class="equipment-value">{{ equipmentNum }}</span>
          <span class="equipment-label">设备总数</span>
        </div>
        <div class="equipment-item">
          <span class="equipment-value">{{ equipmentRepair }}</span>
          <span class="equipment-label">待维修设备</span>
        </div>
        <div class="equipment-item">
          <span class="equipment-value">{{ equipmentMaintain }}</span>
          <span class="equipment-label">待保养设备</span>
        </div>
        <div class="equipment-item">
          <span class="equipment-value">{{ totalMeasuring }}</span>
          <span class="equipment-label">计量器具总数</span>
        </div>
      </div>
    </div>
    <!-- äº‹ä»¶åç§° -->
    <div class="event-info">
      <div class="event-header">
        <img
          src="@/assets/BI/shijianmingxiicon@2x.png"
          alt="图标"
          class="event-icon"
        />
        <span class="event-title">事件名称</span>
      </div>
      <div class="event-content">
        <ul class="todo-list" v-if="todoList.length > 0" ref="refTodoList">
          <li v-for="item in todoList" :key="item.id">
            <div
              style="
                display: flex;
                flex-direction: column;
                justify-content: space-between;
                width: 100%;
                gap: 20px;
              "
            >
            <div class="todo-division">待办事由:{{ item.approveReason }}</div>
              <div style="display: flex;justify-content: space-between;align-items: center;"
              >
                <div class="todo-title">申请类型:{{ item.approveTypeName }}</div>
                <div class="todo-division">申请部门:{{ item.approveDeptName }}</div>
                <div class="todo-time">{{ item.approveTime }}</div>
              </div>
            </div>
          </li>
        </ul>
        <div v-else style="text-align: center">暂无数据</div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { homeTodos, summaryStatistics } from '@/api/viewIndex.js'
import { getLedgerPage } from '@/api/equipmentManagement/ledger.js'
import { getRepairPage } from '@/api/equipmentManagement/repair.js'
import { getUpkeepPage } from '@/api/equipmentManagement/upkeep.js'
import { measuringInstrumentListPage } from '@/api/equipmentManagement/measurementEquipment.js'
// ç»Ÿè®¡æ•°æ®
const totalStaff = ref(0)
const totalCustomers = ref(0)
const totalSuppliers = ref(0)
// åŒæ¯”
const staffYoY = ref(0)
const customersYoY = ref(0)
const suppliersYoY = ref(0)
const equipmentNum = ref(0)
const equipmentRepair = ref(0)
const equipmentMaintain = ref(0)
const totalMeasuring = ref(0)
// å¾…办事项
const todoList = ref([])
const refTodoList = ref(null)
const formatPercent = (val) => {
  const num = Number(val) || 0
  return `${Math.abs(num).toFixed(2)}%`
}
const compareClass = (val) => (val >= 0 ? 'compare-up' : 'compare-down')
// èŽ·å–å‘˜å·¥ã€å®¢æˆ·ã€ä¾›åº”å•†æ•°é‡
const getNum = () => {
  summaryStatistics().then((res) => {
    totalStaff.value = res.data.totalStaff
    staffYoY.value = res.data.staffGrowthRate
    totalCustomers.value = res.data.totalCustomer
    customersYoY.value = res.data.customerGrowthRate
    totalSuppliers.value = res.data.totalSupplier
    suppliersYoY.value = res.data.supplierGrowthRate
  }).catch(err => {
    console.error('获取基础统计数据失败:', err)
  })
}
// èŽ·å–è®¾å¤‡ç›¸å…³æ•°é‡
const getLedgerNum = () => {
  const params = {
    pageNum: -1,
    pageSize: -1,
  }
  getLedgerPage(params).then((res) => {
    equipmentNum.value = res.data.total
  })
  getRepairPage({ ...params, status: 0 }).then((res) => {
    equipmentRepair.value = res.data.total
  })
  getUpkeepPage({ ...params, status: 0 }).then((res) => {
    equipmentMaintain.value = res.data.total
  })
  measuringInstrumentListPage(params).then((res) => {
    totalMeasuring.value = res.data.total
  })
}
// åˆå§‹åŒ–待办事项列表滚动功能
const initTodoListScroll = () => {
  const todoListEl = refTodoList.value
  // å¼ºåˆ¶å¯ç”¨æ»šåŠ¨ï¼Œä¸æ£€æŸ¥ä»»ä½•æ¡ä»¶
  if (todoListEl) {
    // åˆ›å»ºä¸€ä¸ªå…‹éš†é¡¹ï¼Œç”¨äºŽå®žçŽ°æ— ç¼æ»šåŠ¨
    const scrollItems = Array.from(todoListEl.querySelectorAll('li'))
    if (scrollItems.length > 0) {
      // ç¡®ä¿æœ‰è¶³å¤Ÿçš„项目用于滚动
      // å¦‚果项目太少,多复制几次以确保滚动效果
      if (scrollItems.length < 4) {
        const originalItems = [...scrollItems]
        for (let i = 0; i < 4; i++) {
          originalItems.forEach((item) => {
            const clone = item.cloneNode(true)
            todoListEl.appendChild(clone)
          })
        }
        // é‡æ–°èŽ·å–æ‰€æœ‰é¡¹ç›®
        scrollItems.push(
          ...Array.from(todoListEl.querySelectorAll('li')).slice(
            scrollItems.length
          )
        )
      }
      const itemHeight = scrollItems[0]?.offsetHeight || 0
      const containerHeight = todoListEl.clientHeight
      const cloneCount = Math.ceil(containerHeight / itemHeight) + 2
      // å…‹éš†å‰å‡ ä¸ªé¡¹ç›®å¹¶æ·»åŠ åˆ°åˆ—è¡¨æœ«å°¾ï¼Œå®žçŽ°æ— ç¼æ»šåŠ¨
      for (let i = 0; i < cloneCount; i++) {
        const clone = scrollItems[i % scrollItems.length].cloneNode(true)
        todoListEl.appendChild(clone)
      }
      let scrollPosition = 0
      const scrollSpeed = 1.5 // å¢žåŠ æ»šåŠ¨é€Ÿåº¦ï¼Œä½¿æ»šåŠ¨æ›´åŠ æ˜Žæ˜¾
      const pauseTime = 3000 // æ»šåŠ¨æš‚åœæ—¶é—´
      let isPaused = false
      let lastTimestamp = 0
      // è¿žç»­æ»šåŠ¨åŠ¨ç”»å‡½æ•°
      function scrollAnimation(timestamp) {
        if (!lastTimestamp) lastTimestamp = timestamp
        const deltaTime = timestamp - lastTimestamp
        lastTimestamp = timestamp
        if (!isPaused) {
          scrollPosition += scrollSpeed * (deltaTime / 16) // æ ‡å‡†åŒ–为60fps的速度
          // å½“滚动超过原始内容长度时,重置位置实现无缝滚动
          const maxScroll = Math.max(
            todoListEl.scrollHeight -
              containerHeight -
              cloneCount * itemHeight,
            itemHeight * scrollItems.length
          )
          if (scrollPosition >= maxScroll) {
            scrollPosition = 0
            todoListEl.scrollTop = 0
          } else {
            todoListEl.scrollTop = scrollPosition
          }
        }
        todoListEl._animationFrame = requestAnimationFrame(scrollAnimation)
      }
      // å¯åŠ¨æ»šåŠ¨åŠ¨ç”»
      todoListEl._animationFrame = requestAnimationFrame(scrollAnimation)
      // è®¾ç½®æ»šåЍ-暂停-滚动的循环效果
      const pauseTimer = setInterval(() => {
        isPaused = !isPaused
      }, pauseTime)
      // æ¸…理定时器
      todoListEl._pauseTimer = pauseTimer
    }
  }
}
// å¾…办事项
const todoInfoS = () => {
  homeTodos().then((res) => {
    todoList.value = res.data
    // åœ¨èŽ·å–åˆ°å¾…åŠžäº‹é¡¹æ•°æ®åŽï¼Œåˆå§‹åŒ–æ»šåŠ¨åŠŸèƒ½
    nextTick(() => {
      initTodoListScroll()
    })
  })
}
onMounted(() => {
  getNum()
  getLedgerNum()
  todoInfoS()
})
onBeforeUnmount(() => {
  // æ¸…理待办事项列表的动画和定时器
  const todoListEl = refTodoList.value
  if (todoListEl) {
    if (todoListEl._animationFrame) {
      cancelAnimationFrame(todoListEl._animationFrame)
      todoListEl._animationFrame = null
    }
    if (todoListEl._pauseTimer) {
      clearInterval(todoListEl._pauseTimer)
      todoListEl._pauseTimer = null
    }
  }
})
</script>
<style scoped>
.stats-cards {
  display: flex;
  gap: 30px;
}
.stat-card {
  flex: 1;
  display: flex;
  align-items: center;
  background-image: url('@/assets/BI/border@2x.png');
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  height: 142px;
}
.card-icon {
  width: 100px;
  height: 100px;
  margin: 20px 20px 0 10px;
}
.card-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.card-value {
  font-weight: 500;
  font-size: 40px;
  background: linear-gradient(360deg, #008bfd 0%, #ffffff 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
.card-label {
  font-weight: 400;
  font-size: 19px;
  color: rgba(208, 231, 255, 0.7);
}
.card-compare {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 15px;
  color: #d0e7ff;
}
.card-compare > span:first-child {
  font-size: 13px;
  opacity: 0.8;
}
.compare-value {
  font-weight: 600;
}
.compare-icon {
  font-size: 14px;
  position: relative;
  top: -1px; /* è½»å¾®ä¸Šç§»ï¼Œè®©ç®­å¤´ä¸Žæ–‡å­—垂直居中对齐 */
}
.compare-up .compare-value,
.compare-up .compare-icon {
  color: #00c853;
}
.compare-down .compare-value,
.compare-down .compare-icon {
  color: #ff5252;
}
.equipment-stats {
  border: 1px solid #1a58b0;
  padding: 18px;
  height: 240px;
}
.equipment-header {
  font-weight: 500;
  font-size: 21px;
  display: flex;
  border-bottom: 1px solid;
  border-image: linear-gradient(
      270deg,
      rgba(0, 126, 255, 0) 0%,
      rgba(0, 126, 255, 0.4549) 35%,
      #007eff 78%,
      #007eff 100%
    )
    1;
  padding-bottom: 2px;
}
.equipment-title {
  font-weight: 500;
  font-size: 18px;
  background: linear-gradient(360deg, #056dff 0%, #43e8fc 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  line-height: 50px;
}
.equipment-icon {
  width: 50px;
  height: 50px;
}
.equipment-items {
  display: flex;
  justify-content: space-around;
  gap: 30px;
}
.equipment-item {
  text-align: center;
}
.equipment-value {
  display: block;
  font-weight: 500;
  font-size: 40px;
  color: #ffffff;
  width: 120px;
  height: 110px;
  line-height: 110px;
  background-image: url('@/assets/BI/shujutongji@2x.png');
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  margin-bottom: 8px;
}
.equipment-label {
  font-weight: 500;
  font-size: 16px;
  color: #fffffe;
}
.event-info {
  background-image: url('@/assets/BI/shijianmingchengbeijing@2x.png');
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  padding: 20px;
  height: 186px;
}
.event-header {
  display: flex;
  align-items: center;
}
.event-icon {
  width: 40px;
  height: 40px;
}
.event-title {
  font-weight: 500;
  font-size: 18px;
  color: #fffffe;
  line-height: 30px;
}
.todo-list {
  list-style: none;
  padding: 0;
  margin: 0;
  height: 120px; /* æŒ‰ç”¨æˆ·è¦æ±‚调整高度 */
  overflow: hidden;
  font-size: 15px;
}
.todo-list li {
  border-radius: 8px;
  margin-bottom: 12px;
  padding: 12px 40px;
  height: 74px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.todo-title {
  font-weight: 400;
  font-size: 16px;
  color: #fffffe;
  position: relative;
}
.todo-division {
  font-weight: 400;
  font-size: 16px;
  color: #fffffe;
  position: relative;
}
.todo-division::before {
  content: '';
  position: absolute;
  left: -20px;
  top: 50%;
  transform: translateY(-50%);
  width: 6px;
  height: 6px;
  background: #498ceb;
  border-radius: 50%;
}
.todo-time {
  font-weight: 400;
  font-size: 16px;
  color: #fffffe;
  background: rgba(24, 93, 190, 0.4);
border-radius: 5px 5px 5px 5px;
padding: 5px 10px;
}
</style>
src/views/reportAnalysis/dataDashboard/components/basic/left-bottom.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,244 @@
<template>
  <div>
    <PanelHeader title="客户营收贡献数值分析" />
    <div class="main-panel panel-item-customers">
      <div class="filters-row">
        <el-select
          v-model="customerValue"
          class="customer-select"
          placeholder="请选择客户"
          clearable
          filterable
          @change="handleFilterChange"
        >
          <el-option
            v-for="item in customerOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
        <DateTypeSwitch v-model="dateType" @change="handleFilterChange" />
      </div>
      <Echarts
          ref="chart"
          :chartStyle="chartStyle"
          :grid="grid"
          :legend="barLegend"
          :series="barSeries1"
          :tooltip="tooltip"
          :xAxis="xAxis1"
          :yAxis="yAxis1"
          :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
          style="height: 260px"
        />
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '../PanelHeader.vue'
import DateTypeSwitch from '../DateTypeSwitch.vue'
import { customerRevenueAnalysis } from '@/api/viewIndex.js'
import { listCustomer } from '@/api/basicData/customerFile.js'
const dateType = ref(1) // 1=周 2=月 3=季度
const customerValue = ref(null)
const customerOptions = ref([])
// è¥æ”¶åˆ†æžæ•°æ®
const revenueData = ref({
  items: []
})
const chartStyle = {
  width: '100%',
  height: '150%',
}
const grid = {
  left: '3%',
  right: '4%',
  bottom: '3%',
  containLabel: true,
}
const barLegend = {
  show: false,
  textStyle: { color: '#B8C8E0' },
  data: ['营收'],
}
const barSeries1 = ref([
  {
    name: '营收',
    type: 'bar',
    barGap: 0,
    emphasis: {
      focus: 'series',
    },
    itemStyle: {
      color: {
        type: 'linear',
        x: 0,
        y: 1,
        x2: 0,
        y2: 0,
        colorStops: [
          // linear-gradient(360deg, rgba(0,164,237,0) 0%, #4EE4FF 100%)
          { offset: 0, color: 'rgba(0,164,237,0)' },
          { offset: 1, color: '#4EE4FF' },
        ],
      },
    },
    data: [],
  },
])
const tooltip = {
  trigger: 'axis',
  axisPointer: {
    type: 'shadow',
  },
  formatter: function (params) {
    let result = params[0].axisValueLabel + '<br/>'
    params.forEach((item) => {
      result += `<div style="color: #B8C8E0">${item.marker} ${item.seriesName}: ${item.value}</div>`
    })
    return result
  },
}
const xAxis1 = ref([
  {
    type: 'category',
    axisTick: { show: false },
    axisLabel: { color: '#B8C8E0' },
    data: [],
  },
])
const yAxis1 = [
  {
    type: 'value',
    axisLabel: { color: '#B8C8E0' },
  },
]
// èŽ·å–å®¢æˆ·è¥æ”¶åˆ†æžæ•°æ®
const getCustomerRevenueAnalysis = () => {
  if (customerOptions.value.length > 0 && !customerValue.value) {
    // é»˜è®¤é€‰ä¸­ç¬¬ä¸€ä¸ªå®¢æˆ·
    customerValue.value = customerOptions.value[0].value
  }
  if (!customerValue.value) return
  const params = {
    customerId: customerValue.value,
    type: dateType.value
  }
  customerRevenueAnalysis(params)
    .then((res) => {
      xAxis1.value[0].data = []
      barSeries1.value[0].data = []
      const items = res.data?.items || []
      items.forEach((item) => {
        xAxis1.value[0].data.push(item.name)
        barSeries1.value[0].data.push(item.value)
      })
      revenueData.value = res.data
    })
    .catch((error) => {
      console.error('获取客户营收分析失败:', error)
    })
}
const fetchCustomerOptions = async () => {
  try {
    const params = { pageNum: 1, pageSize: 200 }
    const res = await listCustomer(params)
    const records = res?.records || res?.data?.records || res?.rows || []
    customerOptions.value = records.map((r) => ({
      label: r.customerName || r.name || r.customer || '-',
      value: r.id ?? r.customerId ?? r.customerCode ?? r.customerName,
    }))
    // èŽ·å–åˆ°é€‰é¡¹åŽï¼Œå¦‚æžœè¿˜æ²¡é€‰ä¸­ï¼Œé»˜è®¤é€‰ä¸­ç¬¬ä¸€ä¸ª
    if (customerOptions.value.length > 0 && !customerValue.value) {
      customerValue.value = customerOptions.value[0].value
      getCustomerRevenueAnalysis()
    }
  } catch (e) {
    // æŽ¥å£å¼‚常时给一组模拟客户,保证UI可用
    customerOptions.value = [
      { label: '华东精密', value: '华东精密' },
      { label: '星辰电子', value: '星辰电子' },
      { label: '启航科技', value: '启航科技' },
      { label: '铭诚制造', value: '铭诚制造' },
      { label: '远景材料', value: '远景材料' },
    ]
  }
}
const handleFilterChange = () => {
  getCustomerRevenueAnalysis()
}
onMounted(() => {
  fetchCustomerOptions()
})
</script>
<style scoped>
.main-panel {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.filters-row {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}
.customer-select {
  width: 180px;
}
/* ä¸‹æ‹‰æ¡†é£Žæ ¼ï¼šä¸Ž DateTypeSwitch ä¿æŒä¸€è‡´ï¼ˆæ·±è‰²åŠé€æ˜Žã€æµ…色文字、细边框) */
.customer-select :deep(.el-input__wrapper),
.customer-select :deep(.el-select__wrapper) {
  background-color: rgba(26, 88, 176, 0.3);
  border: 1px solid rgba(255, 255, 255, 0.2);
  box-shadow: none;
}
.customer-select :deep(.el-input__inner) {
  color: rgba(184, 200, 224, 0.9);
}
.customer-select :deep(.el-input__inner::placeholder) {
  color: rgba(184, 200, 224, 0.6);
}
.customer-select :deep(.el-select__caret),
.customer-select :deep(.el-icon) {
  color: rgba(184, 200, 224, 0.8);
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 478px;
}
</style>
src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,195 @@
<template>
  <div>
    <PanelHeader title="产品大类" />
    <div class="panel-item-customers">
      <div style="height: 70%">
        <Echarts
          ref="chart"
          :chartStyle="chartStyle"
          :legend="landLegend"
          :series="landSeries"
          :tooltip="landTooltip"
          :color="landColors"
          :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
          style="height: 100%"
          class="land-chart"
        />
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '../PanelHeader.vue'
import { productCategoryDistribution } from '@/api/viewIndex.js'
// æ•°æ®åˆ—表(来自接口)
const dataList = ref([])
// é¢œè‰²åˆ—表
const landColors = ['#26FFCB', '#24CBFF', '#35FBF4', '#2651FF', '#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF']
// label å¯Œæ–‡æœ¬ï¼šä¸ºæ¯ä¸ªé¢œè‰²ç”Ÿæˆä¸€ä¸ªå°åœ†ç‚¹æ ·å¼ï¼ˆç¡®ä¿åœ¨ label ä¸­å¯è§ï¼‰
const dotRich = landColors.reduce((acc, color, idx) => {
  acc[`dot${idx}`] = {
    width: 8,
    height: 8,
    borderRadius: 8,
    backgroundColor: color,
    align: 'center',
  }
  return acc
}, {})
// å›¾ä¾‹é…ç½®ï¼ˆå³ä¾§ç«–排)
const landLegend = {
  show: false,
  icon: 'circle',
  data: [],
  right: '8%',
  top: '40%',
  orient: 'vertical',
  itemGap: 14,
  itemWidth: 6,
  itemHeight: 6,
  textStyle: {
    fontSize: 12,
    rich: {
      unit: {
        color: '#fff',
        fontSize: 12,
        padding: [0, 10, 0, 0],
      },
      text: {
        width: 60,
        color: '#fff',
        fontSize: 12,
      },
    },
  },
  formatter: function (name) {
    const list = dataList.value || []
    const item = list.find((d) => d.name === name)
    if (!item) return name
    const val = Number(item.value || 0)
    const totalValue = list.reduce((sum, it) => sum + Number(it.value || 0), 0)
    const percent = totalValue ? ((val / totalValue) * 100).toFixed(2) : '0.00'
    return `{text|${name}}${val}{unit| å…¬é¡·}${percent}{unit|%}`
  },
}
// æç¤ºæ¡†
const landTooltip = {
  triggerOn: 'click',
  alwaysShowContent: true,
  position: function (pt) {
    return [pt[0], 130]
  },
}
// åŒå±‚环形饼图
const landSeries = ref([
  {
    name: '外圈',
    type: 'pie',
    radius: ['35%', '55%'],
    center: ['50%', '50%'],
    label: {
      show: true,
      position: 'outside',
      color: '#fff',
      fontSize: 12,
      lineHeight: 18,
      rich: {
        ...dotRich,
        parent: { fontSize: 14, fontWeight: 600, color: '#fff', lineHeight: 20 },
        child: { fontSize: 12, color: '#fff', lineHeight: 18 },
      },
      formatter: function (params) {
        const children = params?.data?.children || []
        const parentName = params?.data?.name || ''
        const parentValue = params?.data?.value ?? 0
        const dotKey = `dot${(params?.dataIndex || 0) % landColors.length}`
        const dot = `{${dotKey}|} `
        if (!children.length) return `${dot}{parent|${parentName} ${parentValue}}`
        // å°åœ†ç‚¹ + çˆ¶çº§ name + çˆ¶çº§ value,换行展示 children çš„ name + value
        return [
          `${dot}{parent|${parentName} ${parentValue}}`,
          ...children.map((c) => `{child|${c.name}}`),
        ].join('\n')
      },
    },
    labelLine: {
      show: true,
      length: 20,
      length2: 20,
      lineStyle: {
        color: '#B8C8E0',
      },
    },
    itemStyle: {
      color: function (params) {
        return landColors[params.dataIndex % landColors.length]
      },
    },
    data: dataList.value,
  },
  {
    // å†…圈
    type: 'pie',
    radius: ['35%', '40%'],
    center: ['50%', '50%'],
    silent: true,
    label: {
      show: false,
    },
    labelLine: {
      show: false,
    },
    itemStyle: {
      color: 'rgba(0, 127, 255, 0.25)',
    },
    data: [1],
  },
])
const chartStyle = {
  width: '100%',
  height: '150%',
}
const loadData = async () => {
  try {
    const res = await productCategoryDistribution()
    const items = res?.data?.items || []
    dataList.value = items.map((it) => ({
      name: it.name,
      value: Number(it.value || 0),
      rate: it.rate,
      children: Array.isArray(it.children) ? it.children : [],
    }))
    landLegend.data = dataList.value.map((d) => d.name)
    landSeries.value[0].data = dataList.value
  } catch (e) {
    console.error('获取产品大类分布失败:', e)
    dataList.value = []
    landLegend.data = []
    landSeries.value[0].data = []
  }
}
onMounted(() => {
  loadData()
})
</script>
<style scoped>
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 420px;
}
</style>
src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,329 @@
<template>
  <div>
    <PanelHeader title="客户金额贡献排名" />
    <div class="panel-item-customers">
      <div class="switch-container">
        <DateTypeSwitch v-model="dateType" @change="handleDateTypeChange" />
      </div>
      <Echarts
        ref="chart"
        :chartStyle="chartStyle"
        :grid="grid"
        :legend="{ show: false }"
        :series="series"
        :tooltip="tooltip"
        :xAxis="xAxis"
        :yAxis="yAxis"
        :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
        style="height: 360px"
      />
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '../PanelHeader.vue'
import DateTypeSwitch from '../DateTypeSwitch.vue'
import { customerContributionRanking } from '@/api/viewIndex.js'
const chartStyle = {
  width: '100%',
  height: '100%',
}
const dateType = ref(1) // 1=周 2=月 3=季度
// é£žæœºå›¾æ ‡ SVG path(与 right-top ä¸€è‡´ï¼‰
const aircraft =
  'path://M107.000,71.000 C104.936,71.000 102.665,70.806 100.273,70.467 C94.592,76.922 86.275,81.000 77.000,81.000 C70.794,81.000 65.020,79.170 60.172,76.029 C66.952,74.165 72.647,69.714 76.173,63.817 C69.821,61.362 64.063,58.593 60.000,56.039 L60.000,52.813 C70.456,53.950 80.723,55.000 83.000,55.000 C88.972,55.000 93.000,53.723 93.000,50.000 C93.000,47.071 89.222,45.000 83.000,45.000 C80.723,45.000 70.456,46.050 60.000,47.187 L60.000,43.989 C64.057,41.431 69.807,38.644 76.168,36.173 C72.641,30.281 66.948,25.834 60.172,23.971 C65.020,20.830 70.794,19.000 77.000,19.000 C86.270,19.000 94.584,23.074 100.265,29.524 C102.647,29.191 104.918,29.000 107.000,29.000 C129.644,29.000 148.000,50.000 148.000,50.000 C148.000,50.000 129.644,71.000 107.000,71.000 ZM113.000,38.000 C106.373,38.000 101.000,43.373 101.000,50.000 C101.000,56.627 106.373,62.000 113.000,62.000 C119.627,62.000 125.000,56.627 125.000,50.000 C125.000,43.373 119.627,38.000 113.000,38.000 ZM113.000,56.000 C109.686,56.000 107.000,53.314 107.000,50.000 C107.000,46.686 109.686,44.000 113.000,44.000 C116.314,44.000 119.000,46.686 119.000,50.000 C119.000,53.314 116.314,56.000 113.000,56.000 ZM110.500,19.000 C109.567,19.000 108.763,18.483 108.334,17.726 C100.231,9.857 89.187,5.000 77.000,5.000 C64.813,5.000 53.769,9.857 45.666,17.726 C45.237,18.483 44.433,19.000 43.500,19.000 C42.119,19.000 41.000,17.881 41.000,16.500 C41.000,15.847 41.256,15.259 41.665,14.813 L41.575,14.718 C50.629,5.628 63.156,-0.000 77.000,-0.000 C90.844,-0.000 103.371,5.628 112.425,14.718 L112.335,14.813 C112.744,15.259 113.000,15.847 113.000,16.500 C113.000,17.881 111.881,19.000 110.500,19.000 ZM53.000,49.484 C61.406,48.626 77.810,47.000 81.345,47.000 C87.353,47.000 91.000,48.243 91.000,50.000 C91.000,52.234 87.111,53.000 81.345,53.000 C77.810,53.000 61.406,51.374 53.000,50.516 L53.000,49.484 ZM53.000,47.000 L9.000,50.000 L53.000,53.000 L53.000,56.000 L-0.000,50.000 L53.000,44.000 L53.000,47.000 ZM43.500,81.000 C44.433,81.000 45.237,81.517 45.666,82.274 C53.769,90.143 64.813,95.000 77.000,95.000 C89.187,95.000 100.231,90.143 108.334,82.274 C108.763,81.517 109.567,81.000 110.500,81.000 C111.881,81.000 113.000,82.119 113.000,83.500 C113.000,84.153 112.744,84.741 112.335,85.187 L112.425,85.282 C103.371,94.372 90.844,100.000 77.000,100.000 C63.156,100.000 50.629,94.372 41.575,85.282 L41.665,85.187 C41.256,84.741 41.000,84.153 41.000,83.500 C41.000,82.119 42.119,81.000 43.500,81.000 Z'
// é¢œè‰²é…ç½®ï¼ˆä¸Ž right-top ä¸€è‡´ï¼‰
const color = {
  0: '#ff5676',
  1: '#ffd83e',
  2: '#fbff94',
  3: '#7daeff',
}
// åŽŸå§‹æ•°æ®ï¼ˆç»Ÿä¸€æˆ { NAME, NUM })
const dataArr = ref([])
const dataArray = computed(() => {
  const sortedAsc = [...dataArr.value].sort((a, b) => a.NUM - b.NUM)
  return sortedAsc.length > 5 ? sortedAsc.slice(-5) : sortedAsc
})
const total = computed(() => dataArray.value.reduce((sum, v) => sum + Number(v.NUM || 0), 0))
const xdataName = computed(() => dataArray.value.map((v) => v.NAME))
const dataNum = computed(() => {
  return dataArray.value.map((v, i) => {
    const index = dataArray.value.length - i - 1
    const isTop3 = index < 3
    return {
      value: Number(v.NUM),
      itemStyle: {
        color: {
          type: 'linear',
          x: 1,
          y: 0,
          x2: 0,
          y2: 0,
          colorStops: [
            { offset: 0, color: isTop3 ? '#ffdae1' : '#ecf3ff' },
            { offset: 0.07, color: isTop3 ? color[index] : color[3] },
            {
              offset: 1,
              color: isTop3 ? 'rgba(255, 86, 118, .1)' : 'rgba(125,174,255, .1)',
            },
          ],
          global: false,
        },
        barBorderRadius: [0, 20, 20, 0],
      },
      symbol: isTop3 ? aircraft : 'none',
      symbolPosition: 'end',
      symbolSize: [30, 25],
      symbolOffset: [35, 0],
    }
  })
})
const bgData = computed(() => {
  const maxValue = Math.max(0, ...dataNum.value.map((v) => v.value))
  return dataNum.value.map(() => maxValue + 200)
})
const tooltip = computed(() => ({
  trigger: 'axis',
  textStyle: { fontSize: '100%' },
  formatter: function (params) {
    let result = params[0].axisValueLabel + '<br/>'
    params.forEach((item) => {
      result += `<div style="color: #B8C8E0">${item.marker} ${item.seriesName}: ${item.value}</div>`
    })
    return result
  },
}))
const grid = computed(() => ({ top: 0, left: '20%', right: '10%', bottom: 0 }))
const xAxis = computed(() => [
  {
    splitLine: { show: false },
    axisLine: { show: false },
    axisLabel: { show: false },
    axisTick: { show: false },
  },
])
const yAxis = computed(() => [
  {
    type: 'category',
    inverse: false,
    data: xdataName.value,
    axisLabel: {
      formatter: (value) => {
        if (!value) return ''
        const maxLen = 6 // æ¯è¡Œæœ€å¤šå­—符数,可按需调整
        if (value.length <= maxLen) return `{a|${value}}`
        const lines = []
        for (let i = 0; i < value.length; i += maxLen) {
          lines.push(value.slice(i, i + maxLen))
        }
        return lines.map((line) => `{a|${line}}`).join('\n')
      },
      rich: {
        a: {
          width: 120,
          fontSize: 14,
          color: '#fff',
          padding: [5, 4, 5, 0],
          align: 'right',
        },
      },
    },
    axisLine: { show: false },
    axisTick: { show: false },
    splitLine: { show: false },
  },
  {
    type: 'category',
    data: dataNum.value.map((item) => item.value),
    axisLabel: {
      formatter: (params, index) => {
        const value = typeof params === 'object' ? params.value : params
        const percent = total.value ? ((value / total.value) * 100).toFixed(0) : 0
        const rank = dataArray.value.length - index
        const isTop3 = rank < 4
        return `{a${isTop3 ? rank : ''}|${percent} }{b${isTop3 ? rank : ''}|%}`
      },
      rich: {
        a: { fontSize: 18, color: '#98bfff', verticalAlign: 'bottom' },
        a1: { fontSize: 18, color: '#ff7f97', verticalAlign: 'bottom' },
        a2: { fontSize: 18, color: '#ffce64', verticalAlign: 'bottom' },
        a3: { fontSize: 18, color: '#e8ed66', verticalAlign: 'bottom' },
        b: { fontSize: 12, color: '#98bfff', verticalAlign: 'bottom' },
        b1: { fontSize: 12, color: '#ff7f97', verticalAlign: 'bottom' },
        b2: { fontSize: 12, color: '#ffce64', verticalAlign: 'bottom' },
        b3: { fontSize: 12, color: '#e8ed66', verticalAlign: 'bottom' },
      },
    },
    axisLine: { show: false },
    axisTick: { show: false },
    splitLine: { show: false },
  },
])
const series = computed(() => [
  {
    name: '金额',
    z: 6,
    type: 'pictorialBar',
    data: dataNum.value,
  },
  {
    name: '背景',
    z: 6,
    type: 'bar',
    barWidth: 25,
    tooltip: { show: false },
    itemStyle: {
      color: 'rgba(255,255,255,.1)',
      barBorderRadius: [0, 20, 20, 0],
    },
    data: bgData.value,
  },
  {
    name: '金额渐变',
    type: 'bar',
    barWidth: 25,
    barGap: '-100%',
    tooltip: { show: false },
    itemStyle: {
      color: {
        type: 'linear',
        x: 1,
        y: 0,
        x2: 0,
        y2: 0,
        colorStops: [
          { offset: 0, color: 'rgba(255, 218, 220)' },
          { offset: 0.07, color: 'rgba(255, 86, 118)' },
          { offset: 1, color: 'rgba(255, 86, 118, 0)' },
        ],
        global: false,
      },
      barBorderRadius: [0, 20, 20, 0],
    },
    data: dataNum.value,
  },
])
const normalizeItem = (item) => {
  const name =
    item?.NAME ??
    item?.name ??
    item?.customerName ??
    item?.customer ??
    item?.label ??
    '-'
  const num =
    item?.NUM ??
    item?.num ??
    item?.value ??
    item?.amount ??
    item?.money ??
    0
  return { NAME: String(name), NUM: Number(num) || 0 }
}
const getMockListByType = (type) => {
  // æ¨¡æ‹Ÿå‡æ•°æ®ï¼ˆé‡‘额贡献排名)
  // type: 1=周 2=月 3=季度
  if (type === 2) {
    return [
      { NAME: '华东精密', NUM: 5120000 },
      { NAME: '星辰电子', NUM: 3860000 },
      { NAME: '启航科技', NUM: 2720000 },
      { NAME: '铭诚制造', NUM: 2160000 },
      { NAME: '远景材料', NUM: 1430000 },
      { NAME: '德润贸易', NUM: 910000 },
      { NAME: '宏达配套', NUM: 680000 },
    ]
  }
  if (type === 3) {
    return [
      { NAME: '华东精密', NUM: 16800000 },
      { NAME: '星辰电子', NUM: 12960000 },
      { NAME: '启航科技', NUM: 9720000 },
      { NAME: '铭诚制造', NUM: 7560000 },
      { NAME: '远景材料', NUM: 5430000 },
      { NAME: '德润贸易', NUM: 3910000 },
      { NAME: '宏达配套', NUM: 2680000 },
    ]
  }
  return [
    { NAME: '华东精密', NUM: 1280000 },
    { NAME: '星辰电子', NUM: 860000 },
    { NAME: '启航科技', NUM: 720000 },
    { NAME: '铭诚制造', NUM: 560000 },
    { NAME: '远景材料', NUM: 430000 },
    { NAME: '德润贸易', NUM: 310000 },
    { NAME: '宏达配套', NUM: 180000 },
  ]
}
const setMockData = (type) => {
  dataArr.value = getMockListByType(type).map(normalizeItem)
}
const fetchCustomerRanking = () => {
  customerContributionRanking({ type: dateType.value })
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        dataArr.value = res.data.map(item => ({
          NAME: item.customerName,
          NUM: item.totalAmount
        }))
      } else {
        setMockData(dateType.value)
      }
    })
    .catch((error) => {
      console.error('获取客户金额贡献排名失败:', error)
      setMockData(dateType.value)
    })
}
const handleDateTypeChange = () => {
  fetchCustomerRanking()
}
onMounted(() => {
  fetchCustomerRanking()
})
</script>
<style scoped>
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
}
.switch-container {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 16px;
}
</style>
src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,358 @@
<template>
  <div>
    <PanelHeader title="供应商采购排名" />
    <div class="panel-item-customers">
      <div class="switch-container">
        <DateTypeSwitch v-model="radio1" @change="handleDateTypeChange" />
      </div>
      <Echarts
        ref="chart"
        :chartStyle="chartStyle"
        :grid="grid"
        :series="series"
        :tooltip="tooltip"
        :xAxis="xAxis"
        :yAxis="yAxis"
        :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
        style="height: 360px"
      />
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '../PanelHeader.vue'
import DateTypeSwitch from '../DateTypeSwitch.vue'
import { supplierPurchaseRanking } from '@/api/viewIndex.js'
const chartStyle = {
  width: '100%',
  height: '100%',
}
const radio1 = ref(1)
// é£žæœºå›¾æ ‡ SVG path
const aircraft =
  'path://M107.000,71.000 C104.936,71.000 102.665,70.806 100.273,70.467 C94.592,76.922 86.275,81.000 77.000,81.000 C70.794,81.000 65.020,79.170 60.172,76.029 C66.952,74.165 72.647,69.714 76.173,63.817 C69.821,61.362 64.063,58.593 60.000,56.039 L60.000,52.813 C70.456,53.950 80.723,55.000 83.000,55.000 C88.972,55.000 93.000,53.723 93.000,50.000 C93.000,47.071 89.222,45.000 83.000,45.000 C80.723,45.000 70.456,46.050 60.000,47.187 L60.000,43.989 C64.057,41.431 69.807,38.644 76.168,36.173 C72.641,30.281 66.948,25.834 60.172,23.971 C65.020,20.830 70.794,19.000 77.000,19.000 C86.270,19.000 94.584,23.074 100.265,29.524 C102.647,29.191 104.918,29.000 107.000,29.000 C129.644,29.000 148.000,50.000 148.000,50.000 C148.000,50.000 129.644,71.000 107.000,71.000 ZM113.000,38.000 C106.373,38.000 101.000,43.373 101.000,50.000 C101.000,56.627 106.373,62.000 113.000,62.000 C119.627,62.000 125.000,56.627 125.000,50.000 C125.000,43.373 119.627,38.000 113.000,38.000 ZM113.000,56.000 C109.686,56.000 107.000,53.314 107.000,50.000 C107.000,46.686 109.686,44.000 113.000,44.000 C116.314,44.000 119.000,46.686 119.000,50.000 C119.000,53.314 116.314,56.000 113.000,56.000 ZM110.500,19.000 C109.567,19.000 108.763,18.483 108.334,17.726 C100.231,9.857 89.187,5.000 77.000,5.000 C64.813,5.000 53.769,9.857 45.666,17.726 C45.237,18.483 44.433,19.000 43.500,19.000 C42.119,19.000 41.000,17.881 41.000,16.500 C41.000,15.847 41.256,15.259 41.665,14.813 L41.575,14.718 C50.629,5.628 63.156,-0.000 77.000,-0.000 C90.844,-0.000 103.371,5.628 112.425,14.718 L112.335,14.813 C112.744,15.259 113.000,15.847 113.000,16.500 C113.000,17.881 111.881,19.000 110.500,19.000 ZM53.000,49.484 C61.406,48.626 77.810,47.000 81.345,47.000 C87.353,47.000 91.000,48.243 91.000,50.000 C91.000,52.234 87.111,53.000 81.345,53.000 C77.810,53.000 61.406,51.374 53.000,50.516 L53.000,49.484 ZM53.000,47.000 L9.000,50.000 L53.000,53.000 L53.000,56.000 L-0.000,50.000 L53.000,44.000 L53.000,47.000 ZM43.500,81.000 C44.433,81.000 45.237,81.517 45.666,82.274 C53.769,90.143 64.813,95.000 77.000,95.000 C89.187,95.000 100.231,90.143 108.334,82.274 C108.763,81.517 109.567,81.000 110.500,81.000 C111.881,81.000 113.000,82.119 113.000,83.500 C113.000,84.153 112.744,84.741 112.335,85.187 L112.425,85.282 C103.371,94.372 90.844,100.000 77.000,100.000 C63.156,100.000 50.629,94.372 41.575,85.282 L41.665,85.187 C41.256,84.741 41.000,84.153 41.000,83.500 C41.000,82.119 42.119,81.000 43.500,81.000 Z'
// é¢œè‰²é…ç½®
const color = {
  0: '#ff5676',
  1: '#ffd83e',
  2: '#fbff94',
  3: '#7daeff',
}
// åŽŸå§‹æ•°æ®
const dataArr = ref([])
// æŽ’序后的数据
const dataArray = computed(() => {
  return [...dataArr.value].sort((a, b) => a.NUM - b.NUM)
})
// è®¡ç®—总数
const total = computed(() => {
  return dataArray.value.reduce((sum, v) => sum + Number(v.NUM), 0)
})
// x轴数据(名称)
const xdataName = computed(() => {
  return dataArray.value.map((v) => v.NAME)
})
// y轴数据(数值,带样式)
const dataNum = computed(() => {
  return dataArray.value.map((v, i) => {
    const index = dataArray.value.length - i - 1
    const isTop3 = index < 3
    return {
      value: Number(v.NUM),
      itemStyle: {
        color: {
          type: 'linear',
          x: 1,
          y: 0,
          x2: 0,
          y2: 0,
          colorStops: [
            {
              offset: 0,
              color: isTop3 ? '#ffdae1' : '#ecf3ff',
            },
            {
              offset: 0.07,
              color: isTop3 ? color[index] : color[3],
            },
            {
              offset: 1,
              color: isTop3
                ? 'rgba(255, 86, 118, .1)'
                : 'rgba(125,174,255, .1)',
            },
          ],
          global: false,
        },
        barBorderRadius: [0, 20, 20, 0],
      },
      symbol: isTop3 ? aircraft : 'none',
      symbolPosition: 'end',
      symbolSize: [30, 25],
      symbolOffset: [35, 0],
    }
  })
})
// èƒŒæ™¯æ•°æ®
const bgData = computed(() => {
  const maxValue = Math.max(...dataNum.value.map((v) => v.value))
  return dataNum.value.map(() => maxValue + 200)
})
// tooltip
const tooltip = computed(() => {
  return {
    trigger: 'axis',
    textStyle: { fontSize: '100%' },
    formatter: function (params) {
      let result = params[0].axisValueLabel + '<br/>'
      result += `<div style="">${params[0].marker}${params[0].value}</div>`
      return result
    },
  }
})
// grid
const grid = computed(() => {
  return { top: 0, left: '20%', right: '10%', bottom: 0 }
})
// xAxis
const xAxis = computed(() => {
  return [
    {
      splitLine: { show: false },
      axisLine: { show: false },
      axisLabel: { show: false },
      axisTick: { show: false },
    },
  ]
})
// yAxis
const yAxis = computed(() => {
  return [
    {
      type: 'category',
      inverse: false,
      data: xdataName.value,
      axisLabel: {
        formatter: (value) => {
          if (!value) return ''
          const maxLen = 6 // æ¯è¡Œæœ€å¤šå­—符数,可按需调整
          if (value.length <= maxLen) return `{a|${value}}`
          const lines = []
          for (let i = 0; i < value.length; i += maxLen) {
            lines.push(value.slice(i, i + maxLen))
          }
          // å¤šè¡Œæ–‡æœ¬ï¼Œæ¯è¡Œéƒ½å¥—同一个 rich æ ·å¼
          return lines.map((line) => `{a|${line}}`).join('\n')
        },
        rich: {
          a: {
            width: 120,
            fontSize: 14,
            color: '#fff',
            padding: [5, 4, 5, 0],
            align: 'right',
          },
        },
      },
      axisLine: { show: false },
      axisTick: { show: false },
      splitLine: { show: false },
    },
    {
      type: 'category',
      data: dataNum.value.map((item) => item.value),
      axisLabel: {
        formatter: (params, index) => {
          const value = typeof params === 'object' ? params.value : params
          const percent = ((value / total.value) * 100).toFixed(0)
          const rank = dataArray.value.length - index
          const isTop3 = rank < 4
          return `{a${isTop3 ? rank : ''}|${percent} }{b${isTop3 ? rank : ''}|%}`
        },
        rich: {
          a: {
            fontSize: 18,
            color: '#98bfff',
            verticalAlign: 'bottom',
          },
          a1: {
            fontSize: 18,
            color: '#ff7f97',
            verticalAlign: 'bottom',
          },
          a2: {
            fontSize: 18,
            color: '#ffce64',
            verticalAlign: 'bottom',
          },
          a3: {
            fontSize: 18,
            color: '#e8ed66',
            verticalAlign: 'bottom',
          },
          b: {
            fontSize: 12,
            color: '#98bfff',
            verticalAlign: 'bottom',
          },
          b1: {
            fontSize: 12,
            color: '#ff7f97',
            verticalAlign: 'bottom',
          },
          b2: {
            fontSize: 12,
            color: '#ffce64',
            verticalAlign: 'bottom',
          },
          b3: {
            fontSize: 12,
            color: '#e8ed66',
            verticalAlign: 'bottom',
          },
        },
      },
      axisLine: { show: false },
      axisTick: { show: false },
      splitLine: { show: false },
    },
  ]
})
// series
const series = computed(() => {
  return [
    {
      z: 6,
      type: 'pictorialBar',
      data: dataNum.value,
    },
    {
      z: 6,
      type: 'bar',
      barWidth: 25,
      tooltip: { show: false },
      itemStyle: {
        color: 'rgba(255,255,255,.1)',
        barBorderRadius: [0, 20, 20, 0],
      },
      data: bgData.value,
    },
    {
      type: 'bar',
      barWidth: 25,
      barGap: '-100%',
      tooltip: { show: false },
      itemStyle: {
        color: {
          type: 'linear',
          x: 1,
          y: 0,
          x2: 0,
          y2: 0,
          colorStops: [
            {
              offset: 0,
              color: 'rgba(255, 218, 220)',
            },
            {
              offset: 0.07,
              color: 'rgba(255, 86, 118)',
            },
            {
              offset: 1,
              color: 'rgba(255, 86, 118, 0)',
            },
          ],
          global: false,
        },
        barBorderRadius: [0, 20, 20, 0],
      },
      data: dataNum.value,
    },
  ]
})
// ä¾›åº”商采购排名
const fetchSupplierRanking = () => {
  supplierPurchaseRanking({ type: radio1.value })
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        dataArr.value = res.data.map(item => ({
          NAME: item.supplierName,
          NUM: item.totalAmount
        }))
      } else {
        // å¦‚果没有数据,使用模拟数据
        dataArr.value = [
          { NAME: '供应商A', NUM: 102 },
          { NAME: '供应商B', NUM: 122 },
          { NAME: '供应商C', NUM: 282 },
          { NAME: '供应商D', NUM: 453 },
          { NAME: '供应商E', NUM: 753 },
        ]
      }
    })
    .catch((error) => {
      console.error('获取供应商采购排名失败:', error)
      // ä½¿ç”¨æ¨¡æ‹Ÿæ•°æ®
      dataArr.value = [
        { NAME: '供应商A', NUM: 102 },
        { NAME: '供应商B', NUM: 122 },
        { NAME: '供应商C', NUM: 282 },
        { NAME: '供应商D', NUM: 453 },
        { NAME: '供应商E', NUM: 753 },
      ]
    })
}
// å¤„理日期类型切换
const handleDateTypeChange = (value) => {
  fetchSupplierRanking()
}
onMounted(() => {
  fetchSupplierRanking()
})
</script>
<style scoped>
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
}
.switch-container {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 16px;
}
.section-title {
  font-weight: 500;
  font-size: 16px;
  color: #d9ecff;
}
</style>
src/views/reportAnalysis/dataDashboard/index.vue
@@ -1,329 +1,62 @@
<template>
        <div class="scale-container">
            <div class="data-dashboard" :style="{ transform: `scale(${scaleRatio})` }">
      <!-- å…¨å±æŒ‰é’® - ç§»åŠ¨åˆ°å·¦ä¸Šè§’ -->
      <button class="fullscreen-btn" @click="toggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏显示'">
        <svg v-if="!isFullscreen" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
          <path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>
        </svg>
        <svg v-else width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
          <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
        </svg>
      </button>
  <div class="scale-container">
    <div class="data-dashboard" :style="{ transform: `scale(${scaleRatio})` }">
    <!-- å…¨å±æŒ‰é’® - ç§»åŠ¨åˆ°å·¦ä¸Šè§’ -->
    <button class="fullscreen-btn" @click="toggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏显示'">
      <svg v-if="!isFullscreen" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>
      </svg>
      <svg v-else width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
      </svg>
    </button>
      <!-- é¡¶éƒ¨æ ‡é¢˜æ  -->
      <div class="dashboard-header">
        <div class="factory-name">{{ userStore.currentFactoryName }}</div>
      </div>
      <!-- ä¸»è¦å†…容区域 -->
      <div class="dashboard-content">
      <!-- å·¦ä¾§åŒºåŸŸ -->
      <div class="left-panel">
        <!-- å®¢æˆ·ä¿¡æ¯ç»Ÿè®¡åˆ†æž -->
                <div class="panel-header">
                    <span class="panel-title">在制品统计分析</span>
                </div>
        <div class="panel-item-customers">
                    <div class="quality-cards">
                        <div class="quality-cardSec">
                            <div class="quality-card one"></div>
                            <div class="quality-cardTitle">
                                <div>总在制数量</div>
                                <div>{{workInProcessStatistics.totalQuantity}}ä»¶</div>
                            </div>
                        </div>
                        <div class="quality-cardSec">
                            <div class="quality-card two"></div>
                            <div class="quality-cardTitle">
                                <div>平均周转天数</div>
                                <div>{{workInProcessStatistics.avgTurnoverDays}}天</div>
                            </div>
                        </div>
                        <div class="quality-cardSec">
                            <div class="quality-card three"></div>
                            <div class="quality-cardTitle">
                                <div>周转效率</div>
                                <div>{{workInProcessStatistics.turnoverEfficiency}}%</div>
                            </div>
                        </div>
                    </div>
                    <!-- å·¥åºåœ¨åˆ¶å“æ•°é‡æŸ±çж图 -->
                    <div style="height: 70%">
                        <Echarts ref="chart"
                                         :chartStyle="chartStyle"
                                         :grid="grid"
                                         :legend="workInProcessBarLegend"
                                         :series="workInProcessBarSeries"
                                         :tooltip="tooltip"
                                         :xAxis="workInProcessXAxis"
                                         :yAxis="workInProcessYAxis"
                                         :options="{backgroundColor: 'transparent', textStyle: {color: '#B8C8E0'}}"
                                         style="height: 100%"></Echarts>
                    </div>
        </div>
        <!-- è´¨é‡ç»Ÿè®¡ -->
                <div class="panel-header">
                    <span class="panel-title">近4月质量统计</span>
                </div>
                <div class="main-panel">
                    <div class="panel-item-customers">
                        <div class="quality-cards">
                            <div class="quality-cardSec">
                                <div class="quality-card one"></div>
                                <div class="quality-cardTitle">
                                    <div>原材料检数</div>
                                    <div>{{qualityStatisticsObject.supplierNum}}ä»¶</div>
                                </div>
                            </div>
                            <div class="quality-cardSec">
                                <div class="quality-card two"></div>
                                <div class="quality-cardTitle">
                                    <div>过程检数</div>
                                    <div>{{qualityStatisticsObject.processNum}}ä»¶</div>
                                </div>
                            </div>
                            <div class="quality-cardSec">
                                <div class="quality-card three"></div>
                                <div class="quality-cardTitle">
                                    <div>出厂检数</div>
                                    <div>{{qualityStatisticsObject.factoryNum}}ä»¶</div>
                                </div>
                            </div>
                        </div>
                        <Echarts ref="chart"
                                         :chartStyle="chartStyle"
                                         :grid="grid"
                                         :legend="barLegend"
                                         :series="barSeries1"
                                         :tooltip="tooltip"
                                         :xAxis="xAxis1"
                                         :yAxis="yAxis1"
                                         :options="{backgroundColor: 'transparent', textStyle: {color: '#B8C8E0'}}"
                                         style="height: 260px"></Echarts>
                    </div>
                </div>
      </div>
      <!-- ä¸­é—´åŒºåŸŸ -->
      <div class="center-panel">
        <!-- é¡¶éƒ¨ç»Ÿè®¡å¡ç‰‡ -->
        <div class="stats-cards">
          <div class="stat-card">
            <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
            <div class="card-content">
              <span class="card-label">员工总数</span>
              <span class="card-value">{{totalStaff}}</span>
            </div>
          </div>
          <div class="stat-card">
            <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
            <div class="card-content">
              <span class="card-label">客户总数</span>
              <span class="card-value">{{totalCustomers}}</span>
            </div>
          </div>
          <div class="stat-card">
            <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
            <div class="card-content">
              <span class="card-label">供应商总数</span>
              <span class="card-value">{{totalSuppliers}}</span>
            </div>
          </div>
        </div>
        <!-- è®¾å¤‡ç»Ÿè®¡ -->
        <div class="equipment-stats">
          <div class="equipment-header">
                        <img src="@/assets/BI/shujutongjiicon@2x.png" alt="图标" class="equipment-icon" />
            <span class="equipment-title">设备统计</span>
          </div>
          <div class="equipment-items">
            <div class="equipment-item">
              <span class="equipment-value">{{equipmentNum}}</span>
              <span class="equipment-label">设备总数</span>
            </div>
            <div class="equipment-item">
              <span class="equipment-value">{{equipmentRepair}}</span>
              <span class="equipment-label">待维修设备</span>
            </div>
            <div class="equipment-item">
              <span class="equipment-value">{{equipmentMaintain}}</span>
              <span class="equipment-label">待保养设备</span>
            </div>
            <div class="equipment-item">
              <span class="equipment-value">{{totalMeasuring}}</span>
              <span class="equipment-label">计量器具总数</span>
            </div>
          </div>
        </div>
        <!-- äº‹ä»¶åç§° -->
        <div class="event-info">
          <div class="event-header">
                        <img src="@/assets/BI/shijianmingxiicon@2x.png" alt="图标" class="event-icon" />
            <span class="event-title">事件名称</span>
          </div>
          <div class="event-content">
                        <ul class="todo-list" v-if="todoList.length > 0" ref="refTodoList">
   <li v-for="item in todoList" :key="item.id">
    <div style="display: flex;flex-direction: column;justify-content: space-between;width: 100%;gap: 20px">
     <div style="display: flex;justify-content: space-between;align-items: center;">
      <div class="todo-title">待办编号:{{item.approveId}}</div>
      <div class="todo-division">部门:{{item.approveDeptName}}</div>
      <div class="todo-time">{{item.approveTime}}</div>
     </div>
     <div class="todo-division">待办事由:{{item.approveReason}}</div>
    </div>
   </li>
 </ul>
                        <div v-else style="text-align: center">
                            æš‚无数据
                        </div>
          </div>
        </div>
                <div class="financial-header">
                    <span class="financial-title">各生产订单的完成进度统计</span>
                </div>
                <div class="main-panel">
                    <div class="panel-item-customers">
                        <div class="order-statistics-cards" style="margin-bottom: 0px;">
                            <div class="quality-cardSec">
                                <div class="quality-card four"></div>
                                <div class="quality-cardTitle">
                                    <div>总订单数</div>
                                    <div>{{orderStatisticsObject.totalOrderCount}}ä»¶</div>
                                </div>
                            </div>
                            <div class="quality-cardSec">
                                <div class="quality-card five"></div>
                                <div class="quality-cardTitle">
                                    <div>未完成订单数</div>
                                    <div>{{orderStatisticsObject.uncompletedOrderCount}}ä»¶</div>
                                </div>
                            </div>
                            <div class="quality-cardSec">
                                <div class="quality-card six"></div>
                                <div class="quality-cardTitle">
                                    <div>部分完成订单数</div>
                                    <div>{{orderStatisticsObject.partialCompletedOrderCount}}ä»¶</div>
                                </div>
                            </div>
                            <div class="quality-cardSec">
                                <div class="quality-card seven"></div>
                                <div class="quality-cardTitle">
                                    <div>已完成订单数</div>
                                    <div>{{orderStatisticsObject.completedOrderCount}}ä»¶</div>
                                </div>
                            </div>
                        </div>
                        <div class="progress-table-container" ref="progressTableRef" style="margin-top: 0px;" @scroll="handleTableScroll">
                            <table class="progress-table">
                                <thead>
                                    <tr>
                                        <th>生产订单号</th>
                                        <th>产品名称</th>
                                        <th>规格</th>
                                        <th>需求数量</th>
                                        <th>完成数量</th>
                                        <th>完成进度</th>
                                    </tr>
                                </thead>
                                <tbody>
                                    <tr
                                        v-for="(item, index) in progressTableData"
                                        :key="index"
                                        :ref="el => setRowRef(el, index)"
                                        :class="{ 'row-under-header': isRowUnderHeader(index) }"
                                    >
                                        <td>{{ item.npsNo || '-' }}</td>
                                        <td>{{ item.productCategory || '-' }}</td>
                                        <td>{{ item.specificationModel || '-' }}</td>
                                        <td>{{ item.quantity || 0 }}</td>
                                        <td>{{ item.completeQuantity || 0 }}</td>
                                        <td>
                                            <el-progress
                                                :percentage="calculateProgress(item)"
                                                :color="progressColor(calculateProgress(item))"
                                                :status="calculateProgress(item) >= 100 ? 'success' : ''"
                                                :stroke-width="8"
                                            />
                                        </td>
                                    </tr>
                                </tbody>
                            </table>
                        </div>
                    </div>
                </div>
      </div>
      <!-- å³ä¾§åŒºåŸŸ -->
      <div class="right-panel">
        <!-- åº”收应付统计 -->
                <div class="panel-header">
                    <span class="panel-title">应收应付统计</span>
                </div>
                <div class="panel-item-customers">
                    <div style="display: flex;justify-content: space-between;margin-bottom: 20px;">
                        <div class="section-title">应收应付统计</div>
<!--                        <el-radio-group v-model="radio1" size="large" @change="statisticsReceivable" class="custom-radio-group">-->
<!--                            <el-radio-button label="按周" :value="1" />-->
<!--                            <el-radio-button label="按月" :value="2" />-->
<!--                            <el-radio-button label="按季度" :value="3" />-->
<!--                        </el-radio-group>-->
                    </div>
                    <Echarts ref="chart"
                                     :color="barColors2"
                                     :chartStyle="chartStyle"
                                     :grid="grid"
                   :legend="barLegend2"
                                     :series="barSeries"
                                     :tooltip="tooltip"
                                     :xAxis="xAxis"
                                     :yAxis="yAxis"
                                     :options="{backgroundColor: 'transparent', textStyle: {color: '#B8C8E0'}}"
                                     style="height: 260px"></Echarts>
                </div>
        <!-- å›žæ¬¾ä¸Žå¼€ç¥¨åˆ†æž -->
         <div class="panel-header">
                    <span class="panel-title">近一月回款与开票分析</span>
                </div>
        <div class="panel-item-customers" style="padding-top: 60px;">
                    <Echarts ref="chart" :chartStyle="chartStyle" :grid="grid" :legend="lineLegend" :series="lineSeries"
                                 :tooltip="tooltipLine" :xAxis="xAxis2" :yAxis="yAxis2" :options="{backgroundColor: 'transparent', textStyle: {color: '#FFFFFF'}}" style="height: 270px;"></Echarts>
                </div>
      </div>
      </div>
      </div>
    <!-- é¡¶éƒ¨æ ‡é¢˜æ  -->
    <div class="dashboard-header">
      <div class="factory-name">基础数据</div>
    </div>
    <!-- ä¸»è¦å†…容区域 -->
    <div class="dashboard-content">
    <!-- å·¦ä¾§åŒºåŸŸ -->
    <div class="left-panel">
      <!-- å®¢æˆ·ä¿¡æ¯ç»Ÿè®¡åˆ†æž -->
      <LeftTop />
      <!-- è´¨é‡ç»Ÿè®¡ -->
      <LeftBottom />
    </div>
    <!-- ä¸­é—´åŒºåŸŸ -->
    <div class="center-panel">
      <CenterTop />
      <CenterBottom />
    </div>
    <!-- å³ä¾§åŒºåŸŸ -->
    <div class="right-panel">
      <!-- åº”收应付统计 -->
      <RightTop />
      <!-- å›žæ¬¾ä¸Žå¼€ç¥¨åˆ†æž -->
       <RightBottom />
    </div>
    </div>
    </div>
  </div>
</template>
<script setup>
import * as echarts from 'echarts'
import { ref, reactive, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import autofit from 'autofit.js'
import Echarts from "@/components/Echarts/echarts.vue";
import LeftTop from './components/basic/left-top.vue'
import LeftBottom from './components/basic/left-bottom.vue'
import CenterTop from './components/basic/center-top.vue'
import CenterBottom from './components/basic/center-bottom.vue'
import RightTop from './components/basic/right-top.vue'
import RightBottom from './components/basic/right-bottom.vue'
import useUserStore from '@/store/modules/user'
import {
    analysisCustomerContractAmounts, getAmountHalfYear,
    homeTodos,
    qualityStatistics,
    statisticsReceivablePayable,
    getProgressStatistics,
      getWorkInProcessTurnover
} from "@/api/viewIndex.js";
import {staffOnJobListPage} from "@/api/personnelManagement/employeeRecord.js";
import {listCustomer} from "@/api/basicData/customerFile.js";
import {listSupplier} from "@/api/basicData/supplierManageFile.js";
import {getLedgerPage} from "@/api/equipmentManagement/ledger.js";
import {getRepairPage} from "@/api/equipmentManagement/repair.js";
import {getUpkeepPage} from "@/api/equipmentManagement/upkeep.js";
import {measuringInstrumentListPage} from "@/api/equipmentManagement/measurementEquipment.js";
import {listPageAnalysis} from "@/api/financialManagement/expenseManagement.js";
import {productOrderListPage} from "@/api/productionManagement/productionOrder.js";
// å…¨å±ç›¸å…³çŠ¶æ€
const isFullscreen = ref(false);
@@ -337,431 +70,20 @@
// ç”¨æˆ·store
const userStore = useUserStore()
// å“åº”式数据
const currentTime = ref('')
const currentDate = ref('')
const timer = ref(null)
const charts = ref([])
// å›¾è¡¨å¼•用
const customerPieChartRef = ref(null)
const salesBarChartRef = ref(null)
const dataBarChartRef = ref(null)
const financialAreaChartRef = ref(null)
const realtimeLineChartRef = ref(null)
const refContractList = ref(null)
const refTodoList = ref(null)
const progressTableRef = ref(null)
const timerScroll = ref(null)
const progressTableScrollTimer = ref(null)
const isTableScrolling = ref(false)
const tableScrollTimeout = ref(null)
const tableRowRefs = ref([])
const rowsUnderHeader = ref(new Set())
const chartStylePie = {
    width: '100%',
    height: '100%' // è®¾ç½®å›¾è¡¨å®¹å™¨çš„高度
}
const materialPieSeries = ref([
    {
        type: 'pie',
        radius: ['0%', '90%'],
        avoidLabelOverlap: false,
        itemStyle: {
            borderColor: '#fff',
            borderWidth: 0
        },
        label: {
            show: false
        },
        data: []
    }
])
const pieLegend = reactive({
    show: false,
})
const sum = ref(0)
const totalStaff = ref(0)
const totalCustomers = ref(0)
const totalSuppliers = ref(0)
const yny = ref(0)
const chain = ref(0)
const equipmentNum = ref(0)
const equipmentRepair = ref(0)
const equipmentMaintain = ref(0)
const totalMeasuring = ref(0)
const pieTooltip = reactive({
    trigger: 'item',
    formatter: function (params) {
        // åŠ¨æ€ç”Ÿæˆæç¤ºä¿¡æ¯ï¼ŒåŸºäºŽæ•°æ®é¡¹çš„ name å±žæ€§
        const description = params.name === '本月回款金额' ? '本月回款金额' : '应收款金额';
        return `<div style="color: #B8C8E0">${description} ${params.value}元 ${params.percent}%</div>`;
    },
    position: 'right'
})
const qualityStatisticsObject = ref({
    supplierNum: 0,
    processNum: 0,
    factoryNum: 0,
})
// è®¢å•统计对象
const orderStatisticsObject = ref({
    totalOrderCount: 0,
    uncompletedOrderCount: 0,
    partialCompletedOrderCount: 0,
    completedOrderCount: 0,
})
// åœ¨åˆ¶å“å‘¨è½¬ç»Ÿè®¡å¯¹è±¡
const workInProcessStatistics = ref({
    totalQuantity: 0,
    avgTurnoverDays: 0,
    turnoverEfficiency: 0,
})
const chartStyle = {
    width: '100%',
    height: '150%' // è®¾ç½®å›¾è¡¨å®¹å™¨çš„高度
}
const barSeries = ref([
    {
        name: '应付金额',
        type: 'bar',
        data: [],
        label: {
            show: true,
        },
        itemStyle: {
            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                { offset: 0, color: '#00A4ED' },
                { offset: 1, color: '#4EE4FF' }
            ])
        }
    },
    {
        name: '应收金额',
        type: 'bar',
        data: [],
        label: {
            show: true,
        },
        itemStyle: {
            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                { offset: 0, color: '#537EF5' },
                { offset: 1, color: '#9061F8' }
            ])
        }
    }
])
const radio1 = ref(1)
const barColors2 = ['#5181DB', '#D369E0', '#F2CA6D', '#60CCA8']
const grid = {
    left: '3%',
    right: '4%',
    bottom: '3%',
    containLabel: true
}
const lineLegend = {
    show: true,
  textStyle: { color: '#B8C8E0' },
    data: ['开票', '回款']
}
const lineSeries = ref([
    {
        type: 'line',
        data: [],
        label: {
            show: true
        },
        showSymbol: true, // æ˜¾ç¤ºåœ†ç‚¹
    },
])
const tooltipLine = {
    trigger: 'axis',
}
const yAxis2 = ref([
    {
        type: 'value',
    }
])
const xAxis2 = ref([
    {
        type: 'category',
        data: [],
        axisLabel: {
            interval: 0,
            formatter: function(value) {
                return value.replace(/~/g, '\n');
            },
        }
    }
])
const barLegend2 = {
    show: true,
    textStyle: { color: '#B8C8E0' },
    data: ['应付金额', '应收金额']
}
const barLegend = {
    show: true,
    textStyle: { color: '#B8C8E0' },
    data: ['原材料合格数', '过程合格数', '出不合格数']
}
const barLegend1 = {
    show: false,
    textStyle: { color: '#B8C8E0' },
    data: []
}
const barSeries11 = ref([
    {
        name: '生产订单统计',
        type: 'bar',
        barGap: 0,
        emphasis: {
            focus: 'series'
        },
        itemStyle: {
            // ä½¿ç”¨å‡½æ•°æ ¹æ®æ•°æ®ç´¢å¼•返回不同颜色
            color: function(params) {
                const colorStops = [
                    [
                        { offset: 1, color: '#00A4ED' },
                        { offset: 0, color: '#4EE4FF' }
                    ],
                    [
                        { offset: 1, color: '#3378FF' },
                        { offset: 0, color: '#4E8AFF' }
                    ],
                    [
                        { offset: 1, color: '#FF6B6B' },
                        { offset: 0, color: '#FF8E8E' }
                    ],
                    [
                        { offset: 1, color: '#537EF5' },
                        { offset: 0, color: '#9061F8' }
                    ]
                ]
                const stops = colorStops[params.dataIndex] || colorStops[0]
                return {
                    type: 'linear',
                    x: 0,
                    y: 0,
                    x2: 0,
                    y2: 1,
                    colorStops: stops
                }
            }
        },
        data: []
    }
])
const barSeries1 = ref([
    {
        name: '原材料合格数',
        type: 'bar',
        barGap: 0,
        emphasis: {
            focus: 'series'
        },
        itemStyle: {
            color: {
                type: 'linear',
                x: 0,
                y: 0,
                x2: 0,
                y2: 1,
                colorStops: [
                    { offset: 1, color: '#00A4ED' },
                    { offset: 0, color: '#4EE4FF' }
                ]
            }
        },
        data: []
    },
    {
        name: '过程合格数',
        type: 'bar',
        emphasis: {
            focus: 'series'
        },
        itemStyle: {
            color: {
                type: 'linear',
                x: 0,
                y: 0,
                x2: 0,
                y2: 1,
                colorStops: [
                    { offset: 1, color: '#3378FF' },
                    { offset: 0, color: '#4E8AFF' }
                ]
            }
        },
        data: []
    },
    {
        name: '出厂合格数',
        type: 'bar',
        emphasis: {
            focus: 'series'
        },
        itemStyle: {
            color: {
                type: 'linear',
                x: 0,
                y: 0,
                x2: 0,
                y2: 1,
                colorStops: [
                    { offset: 1, color: '#537EF5' },
                    { offset: 0, color: '#9061F8' }
                ]
            }
        },
        data: []
    },
])
const tooltip = {
    trigger: 'axis',
    axisPointer: {
        type: 'shadow'
    },
    formatter: function (params) {
        let result = params[0].axisValueLabel + '<br/>';
        params.forEach(item => {
            result += `<div style="color: #B8C8E0">${item.marker} ${item.seriesName}: ${item.value}</div>`;
        });
        return result;
    }
}
const xAxis = [{
    type: 'value',
}]
const yAxis = [{
    type: 'category',
    data: ['应收应付统计']
}]
const xAxis1 = ref([{
    type: 'category',
    axisTick: { show: false },
    axisLabel: { color: '#B8C8E0' },
    data: []
}])
const yAxis1 = [{
    type: 'value',
    axisLabel: { color: '#B8C8E0' }
}]
const xAxis3 = ref([{
    type: 'category',
    axisTick: { show: false },
    axisLabel: { color: '#B8C8E0' },
    data: []
}])
const yAxis3 = [{
    type: 'value',
    axisLabel: { color: '#B8C8E0' }
}]
// åœ¨åˆ¶å“å·¥åºæŸ±çŠ¶å›¾é…ç½®
const workInProcessXAxis = ref([{
    type: 'category',
    axisTick: { show: false },
    axisLabel: { color: '#B8C8E0' },
    data: []
}])
const workInProcessYAxis = [{
    type: 'value',
    axisLabel: { color: '#B8C8E0' },
    name: ''
}]
const workInProcessBarLegend = {
    show: false,
    textStyle: { color: '#B8C8E0' },
    data: []
}
const workInProcessBarSeries = ref([
    {
        name: '在制品数量',
        type: 'bar',
        barWidth: 25, // å›ºå®šæŸ±çŠ¶å›¾å®½åº¦ä¸º40px
        barGap: 0,
        emphasis: {
            focus: 'series'
        },
        itemStyle: {
            color: {
                type: 'linear',
                x: 0,
                y: 0,
                x2: 0,
                y2: 1,
                colorStops: [
                    { offset: 0, color: '#4EE4FF' },
                    { offset: 1, color: '#00A4ED' }
                ]
            }
        },
        label: {
            show: true,
            position: 'top',
            color: '#B8C8E0'
        },
        data: []
    }
])
// å¾…办事项
const todoList = ref([])
// ç”Ÿäº§è®¢å•完成进度表格数据
const progressTableData = ref([])
// è®¡ç®—完成进度百分比
const calculateProgress = (item) => {
    if (!item) return 0
    // ä¼˜å…ˆä½¿ç”¨completionStatus字段
    if (item.completionStatus !== undefined && item.completionStatus !== null) {
        const percentage = Number(item.completionStatus)
        if (isNaN(percentage)) return 0
        return Math.min(Math.max(Math.round(percentage), 0), 100)
    }
    // å¦‚果没有completionStatus,则根据完成数量和需求数量计算
    if (!item.quantity || item.quantity === 0) return 0
    const percentage = (item.completeQuantity || 0) / item.quantity * 100
    return Math.min(Math.max(Math.round(percentage), 0), 100)
}
// æ ¹æ®è¿›åº¦ç™¾åˆ†æ¯”返回颜色
const progressColor = (percentage) => {
    const p = percentage || 0
    if (p < 30) return "#f56c6c"
    if (p < 50) return "#e6a23c"
    if (p < 80) return "#409eff"
    return "#67c23a"
}
// è®¡ç®—缩放比例
const calculateScale = () => {
  const container = document.querySelector('.scale-container')
  if (!container) return
  // èŽ·å–å®¹å™¨çš„å®žé™…å°ºå¯¸
    const rect = container.getBoundingClientRect?.()
    const containerWidth = container.clientWidth || rect?.width || window.innerWidth
    const containerHeight = container.clientHeight || rect?.height || window.innerHeight
  const rect = container.getBoundingClientRect?.()
  const containerWidth = container.clientWidth || rect?.width || window.innerWidth
  const containerHeight = container.clientHeight || rect?.height || window.innerHeight
  // è®¡ç®—宽高缩放比例,取较小值以保证内容完整显示(等比缩放)
  const scaleX = containerWidth / designWidth
  const scaleY = containerHeight / designHeight
  scaleRatio.value = Math.min(scaleX, scaleY)
  // è§¦å‘图表resize
  charts.value.forEach(chart => {
    if (chart && chart.resize) {
      chart.resize()
    }
  })
}
// çª—口大小变化处理
@@ -772,529 +94,29 @@
  }, 100)
}
// é”€æ¯å›¾è¡¨å®žä¾‹
const disposeCharts = () => {
  charts.value.forEach(chart => {
    if (chart && chart.dispose) {
      chart.dispose()
    }
  })
  charts.value = []
}
// åˆåŒé‡‘额
const analysisCustomer = () => {
    analysisCustomerContractAmounts().then((res) => {
        sum.value = res.data.sum
        yny.value = res.data.yny
        chain.value = res.data.chain
        // ä¸ºæ¯ä¸ªæ•°æ®é¡¹åˆ†é…éšæœºé¢œè‰²
        materialPieSeries.value[0].data = res.data.item.map(item => ({
            ...item,
            itemStyle: { color: getRandomColor() }
        }))
    })
}
// åœ¨åˆ¶å“å‘¨è½¬ç»Ÿè®¡
const workInProcessTurnoverInfo = () => {
    getWorkInProcessTurnover().then((res) => {
        console.log("在制品周转统计数据:", res)
        if (!res || !res.data) {
            console.warn('在制品周转统计数据为空')
            return
        }
        // ä»ŽæŽ¥å£èŽ·å–ç»Ÿè®¡æ•°æ®
        workInProcessStatistics.value = {
            totalQuantity: res.data.totalOrderCount || 0,
            avgTurnoverDays: res.data.averageTurnoverDays || 0,
            turnoverEfficiency: res.data.turnoverEfficiency || 0,
        }
        // è®¾ç½®å·¥åºæŸ±çŠ¶å›¾æ•°æ®
        // X轴:processDetails (工序详情数组)
        // Y轴:processQuantityDetails (工序数量详情数组)
        if (res.data.processDetails && Array.isArray(res.data.processDetails)) {
            // è®¾ç½®X轴数据(工序名称)
            workInProcessXAxis.value[0].data = res.data.processDetails
        } else {
            workInProcessXAxis.value[0].data = []
        }
        if (res.data.processQuantityDetails && Array.isArray(res.data.processQuantityDetails)) {
            // è®¾ç½®Y轴数据(在制品数量)
            workInProcessBarSeries.value[0].data = res.data.processQuantityDetails
        } else {
            workInProcessBarSeries.value[0].data = []
        }
    }).catch((error) => {
        console.error('获取在制品周转统计失败:', error)
    })
}
// è´¨æ£€ç»Ÿè®¡
const qualityStatisticsInfo = () => {
    qualityStatistics().then((res) => {
        res.data.item.forEach(item => {
            xAxis1.value[0].data.push(item.date)
            barSeries1.value[0].data.push(item.supplierNum)
            barSeries1.value[1].data.push(item.processNum)
            barSeries1.value[2].data.push(item.factoryNum)
        })
        qualityStatisticsObject.value.supplierNum = res.data.supplierNum
        qualityStatisticsObject.value.processNum = res.data.processNum
        qualityStatisticsObject.value.factoryNum = res.data.factoryNum
    })
}
// å„生产订单的完成进度统计
const progressStatisticsInfo = () => {
    // ä»Žç»Ÿè®¡æŽ¥å£èŽ·å–ç»Ÿè®¡æ•°æ®
    getProgressStatistics().then((res) => {
        console.log("生产订单完成进度统计数据:", res)
        if (!res || !res.data) {
            console.warn('生产订单完成进度统计数据为空')
            return
        }
        // ä»ŽæŽ¥å£èŽ·å–ç»Ÿè®¡æ•°æ®
        orderStatisticsObject.value = {
            totalOrderCount: res.data.totalOrderCount || 0,
            uncompletedOrderCount: res.data.uncompletedOrderCount || 0,
            partialCompletedOrderCount: res.data.partialCompletedOrderCount || 0,
            completedOrderCount: res.data.completedOrderCount || 0
        }
        progressTableData.value = res.data.completedOrderDetails || []
        // é‡ç½®è¡Œå¼•用
        tableRowRefs.value = []
        rowsUnderHeader.value.clear()
        // åœ¨èŽ·å–åˆ°æ•°æ®åŽï¼Œåˆå§‹åŒ–æ»šåŠ¨åŠŸèƒ½
        nextTick(() => {
            initProgressTableScroll()
        })
    }).catch((error) => {
        console.error('获取生产订单完成进度统计失败:', error)
    })
}
// è´¢åŠ¡ç»Ÿè®¡
// const accountStatisticsInfo = () => {
//     listPageAnalysis().then((res) => {
//         xAxis3.value[0].data = res.data.days
//         barSeries11.value[0].data = res.data.totalIncome
//     })
// }
const getNum = () => {
    const params = {
        pageNum: -1,
        pageSize: -1,
    }
    staffOnJobListPage({...params, staffState: 1}).then(res => {
        totalStaff.value = res.data.total
    })
    listCustomer(params).then((res) => {
        totalCustomers.value = res.total;
    });
    listSupplier(params).then((res) => {
        totalSuppliers.value = res.data.total
    });
}
const getLedgerNum = () => {
    const params = {
        pageNum: -1,
        pageSize: -1,
    }
    getLedgerPage(params).then((res) => {
        equipmentNum.value = res.data.total
    });
    getRepairPage({...params, status:0}).then((res) => {
        equipmentRepair.value = res.data.total
    });
    getUpkeepPage({...params, status:0}).then((res) => {
        equipmentMaintain.value = res.data.total
    });
    measuringInstrumentListPage(params).then((res) => {
        totalMeasuring.value = res.data.total
    });
}
// å¾…办事项
const todoInfoS = () => {
    homeTodos().then((res) => {
        todoList.value = res.data
        // åœ¨èŽ·å–åˆ°å¾…åŠžäº‹é¡¹æ•°æ®åŽï¼Œåˆå§‹åŒ–æ»šåŠ¨åŠŸèƒ½
        nextTick(() => {
            initTodoListScroll()
        })
    })
}
// åº”付应收统计
const statisticsReceivable = (type) => {
    statisticsReceivablePayable({type: radio1.value}).then((res) => {
        // è®¾ç½®åº”付金额数据
        barSeries.value[0].data = [
            { value: res.data.payableMoney }
        ]
        // è®¾ç½®åº”收金额数据
        barSeries.value[1].data = [
            { value: res.data.receivableMoney }
        ]
    })
}
const getAmountHalfYearNum = async () => {
    const res = await getAmountHalfYear()
    console.log(res)
    const monthName = []
    const receiptAmount = []
    const invoiceAmount = []
    res.data.forEach(item => {
        monthName.push(item.month)
        receiptAmount.push(item.receiptAmount)
        invoiceAmount.push(item.invoiceAmount)
    })
    // æ­£ç¡®å“åº”式赋值:创建新的 xAxis å’Œ series å¯¹è±¡
    xAxis2.value[0].data = monthName
    xAxis2.value[0].data = monthName.map(item => item.replace(/~/g, '\n~'));
    lineSeries.value = [
        {
            name: '开票',
            type: 'line',
            data: receiptAmount,
            stack: 'Total',
            areaStyle: {
                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                    {
                        offset: 0,
                        color: 'rgba(131, 207, 255, 1)'
                    },
                    {
                        offset: 1,
                        color: 'rgba(186, 228, 255, 1)'
                    }
                ])
            },
            itemStyle: {
                color: '#2D99FF',
                borderColor: '#2D99FF'
            },
            emphasis: {
                focus: 'series'
            },
            lineStyle: {
                width: 0
            },
            showSymbol: true,
        },
        {
            name: '回款',
            type: 'line',
            data: invoiceAmount,
            stack: 'Total',
            lineStyle: {
                width: 0
            },
            itemStyle: {
                color: '#83CFFF',
                borderColor: '#83CFFF'
            },
            showSymbol: true,
            areaStyle: {
                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                    {
                        offset: 0,
                        color: 'rgba(54, 153, 255, 1)'
                    },
                    {
                        offset: 1,
                        color: 'rgba(89, 169, 254, 1)'
                    }
                ])
            },
            emphasis: {
                focus: 'series'
            },
        }
    ]
}
// è‡ªåŠ¨è½®æ¢å‘¨ã€æœˆã€å­£åº¦çš„å®šæ—¶å™¨
const autoSwitchTimer = ref(null)
// è®¾ç½®è¡Œå¼•用
const setRowRef = (el, index) => {
    if (el) {
        tableRowRefs.value[index] = el
    }
}
// åˆ¤æ–­è¡Œæ˜¯å¦åœ¨è¡¨å¤´ä¸‹æ–¹
const isRowUnderHeader = (index) => {
    return rowsUnderHeader.value.has(index)
}
// å¤„理表格滚动事件
const handleTableScroll = () => {
    const tableContainer = progressTableRef.value
    if (!tableContainer) return
    const thead = tableContainer.querySelector('thead')
    if (!thead) return
    const theadHeight = thead.offsetHeight
    const containerRect = tableContainer.getBoundingClientRect()
    const containerTop = containerRect.top
    const theadBottom = containerTop + theadHeight
    // æ¸…空之前的记录
    rowsUnderHeader.value.clear()
    // æ£€æŸ¥æ¯ä¸€è¡Œæ˜¯å¦åœ¨è¡¨å¤´ä¸‹æ–¹ï¼ˆè¢«è¡¨å¤´é®æŒ¡ï¼‰
    tableRowRefs.value.forEach((row, index) => {
        if (row) {
            const rowRect = row.getBoundingClientRect()
            const rowTop = rowRect.top
            const rowBottom = rowRect.bottom
            // å¦‚果行与表头有重叠(行在表头下方被遮挡)
            // è¡Œçš„顶部在表头底部下方,但行的底部在表头底部上方,说明被遮挡
            if (rowTop < theadBottom && rowBottom > containerTop) {
                rowsUnderHeader.value.add(index)
            }
        }
    })
    // æ¸…除之前的定时器
    if (tableScrollTimeout.value) {
        clearTimeout(tableScrollTimeout.value)
    }
    // æ»šåŠ¨åœæ­¢åŽæ¸…ç©ºæ·¡åŒ–æ ‡è®°
    tableScrollTimeout.value = setTimeout(() => {
        rowsUnderHeader.value.clear()
    }, 150)
}
// åˆå§‹åŒ–生产订单进度表格滚动功能
const initProgressTableScroll = () => {
    const tableContainer = progressTableRef.value
    if (!tableContainer) return
    // æ¸…理之前的滚动动画和定时器
    if (progressTableScrollTimer.value) {
        cancelAnimationFrame(progressTableScrollTimer.value)
        progressTableScrollTimer.value = null
    }
    if (tableContainer._pauseTimer) {
        clearInterval(tableContainer._pauseTimer)
        tableContainer._pauseTimer = null
    }
    const tbody = tableContainer.querySelector('tbody')
    if (!tbody) return
    // æ¸…理之前可能存在的克隆行(保留原始数据行)
    // åŽŸå§‹æ•°æ®è¡Œçš„æ•°é‡åº”è¯¥ç­‰äºŽ progressTableData.value.length
    const originalCount = progressTableData.value.length
    const allRows = Array.from(tbody.querySelectorAll('tr'))
    if (allRows.length > originalCount) {
        // ç§»é™¤æ‰€æœ‰è¶…过原始数量的行(这些是克隆的行)
        for (let i = originalCount; i < allRows.length; i++) {
            allRows[i].remove()
        }
    }
    const scrollItems = Array.from(tbody.querySelectorAll('tr'))
    if (scrollItems.length === 0) return
    // èŽ·å–åŽŸå§‹æ•°æ®é¡¹æ•°é‡
    const originalItemCount = scrollItems.length
    // è®¡ç®—容器高度和表头高度
    const thead = tableContainer.querySelector('thead')
    const theadHeight = thead ? thead.offsetHeight : 40
    const containerHeight = tableContainer.clientHeight
    const visibleHeight = containerHeight - theadHeight
    // è®¡ç®—原始数据的总高度
    const itemHeight = scrollItems[0]?.offsetHeight || 40
    const totalContentHeight = itemHeight * originalItemCount
    // å¦‚果数据量不够,容器可以完全显示所有数据,就不需要滚动和克隆
    if (totalContentHeight <= visibleHeight) {
        // æ•°æ®é‡å°‘,不需要滚动,直接返回
        return
    }
    // æ•°æ®é‡è¶³å¤Ÿï¼Œéœ€è¦æ»šåŠ¨ï¼Œè¿›è¡Œå…‹éš†ä»¥å®žçŽ°æ— ç¼æ»šåŠ¨
    const cloneCount = Math.ceil(visibleHeight / itemHeight) + 2
    // å…‹éš†å‰å‡ ä¸ªé¡¹ç›®å¹¶æ·»åŠ åˆ°åˆ—è¡¨æœ«å°¾ï¼Œå®žçŽ°æ— ç¼æ»šåŠ¨
    for (let i = 0; i < cloneCount; i++) {
        const clone = scrollItems[i % originalItemCount].cloneNode(true)
        tbody.appendChild(clone)
    }
    let scrollPosition = 0
    const scrollSpeed = 1.5
    const pauseTime = 3000
    let isPaused = false
    let lastTimestamp = 0
    // è¿žç»­æ»šåŠ¨åŠ¨ç”»å‡½æ•°
    function scrollAnimation(timestamp) {
        if (!lastTimestamp) lastTimestamp = timestamp
        const deltaTime = timestamp - lastTimestamp
        lastTimestamp = timestamp
        if (!isPaused) {
            scrollPosition += scrollSpeed * (deltaTime / 16)
            // è®¡ç®—最大滚动位置(原始内容的高度)
            const maxScroll = itemHeight * originalItemCount
            // å½“滚动超过原始内容长度时,重置位置实现无缝滚动
            if (scrollPosition >= maxScroll) {
                scrollPosition = 0
                tableContainer.scrollTop = 0
            } else {
                tableContainer.scrollTop = scrollPosition
            }
        }
        progressTableScrollTimer.value = requestAnimationFrame(scrollAnimation)
    }
    // å¯åŠ¨æ»šåŠ¨åŠ¨ç”»
    progressTableScrollTimer.value = requestAnimationFrame(scrollAnimation)
    // è®¾ç½®æ»šåЍ-暂停-滚动的循环效果
    const pauseTimer = setInterval(() => {
        isPaused = !isPaused
    }, pauseTime)
    // æ¸…理定时器
    tableContainer._pauseTimer = pauseTimer
}
// åˆå§‹åŒ–待办事项列表滚动功能
const initTodoListScroll = () => {
    const todoList = refTodoList.value
    // å¼ºåˆ¶å¯ç”¨æ»šåŠ¨ï¼Œä¸æ£€æŸ¥ä»»ä½•æ¡ä»¶
    if (todoList) {
        // åˆ›å»ºä¸€ä¸ªå…‹éš†é¡¹ï¼Œç”¨äºŽå®žçŽ°æ— ç¼æ»šåŠ¨
        const scrollItems = Array.from(todoList.querySelectorAll('li'))
        if (scrollItems.length > 0) {
            // ç¡®ä¿æœ‰è¶³å¤Ÿçš„项目用于滚动
            // å¦‚果项目太少,多复制几次以确保滚动效果
            if (scrollItems.length < 4) {
                const originalItems = [...scrollItems]
                for (let i = 0; i < 4; i++) {
                    originalItems.forEach(item => {
                        const clone = item.cloneNode(true)
                        todoList.appendChild(clone)
                    })
                }
                // é‡æ–°èŽ·å–æ‰€æœ‰é¡¹ç›®
                scrollItems.push(...Array.from(todoList.querySelectorAll('li')).slice(scrollItems.length));
            }
            const itemHeight = scrollItems[0]?.offsetHeight || 0
            const containerHeight = todoList.clientHeight
            const cloneCount = Math.ceil(containerHeight / itemHeight) + 2
            // å…‹éš†å‰å‡ ä¸ªé¡¹ç›®å¹¶æ·»åŠ åˆ°åˆ—è¡¨æœ«å°¾ï¼Œå®žçŽ°æ— ç¼æ»šåŠ¨
            for (let i = 0; i < cloneCount; i++) {
                const clone = scrollItems[i % scrollItems.length].cloneNode(true)
                todoList.appendChild(clone)
            }
            let scrollPosition = 0
            const scrollSpeed = 1.5 // å¢žåŠ æ»šåŠ¨é€Ÿåº¦ï¼Œä½¿æ»šåŠ¨æ›´åŠ æ˜Žæ˜¾
            const pauseTime = 3000 // æ»šåŠ¨æš‚åœæ—¶é—´
            let isPaused = false
            let lastTimestamp = 0
            // è¿žç»­æ»šåŠ¨åŠ¨ç”»å‡½æ•°
            function scrollAnimation(timestamp) {
                if (!lastTimestamp) lastTimestamp = timestamp
                const deltaTime = timestamp - lastTimestamp
                lastTimestamp = timestamp
                if (!isPaused) {
                    scrollPosition += scrollSpeed * (deltaTime / 16) // æ ‡å‡†åŒ–为60fps的速度
                    // å½“滚动超过原始内容长度时,重置位置实现无缝滚动
                    const maxScroll = Math.max(todoList.scrollHeight - containerHeight - cloneCount * itemHeight, itemHeight * scrollItems.length)
                    if (scrollPosition >= maxScroll) {
                        scrollPosition = 0
                        todoList.scrollTop = 0
                    } else {
                        todoList.scrollTop = scrollPosition
                    }
                }
                todoList._animationFrame = requestAnimationFrame(scrollAnimation)
            }
            // å¯åŠ¨æ»šåŠ¨åŠ¨ç”»
            todoList._animationFrame = requestAnimationFrame(scrollAnimation)
            // è®¾ç½®æ»šåЍ-暂停-滚动的循环效果
            const pauseTimer = setInterval(() => {
                isPaused = !isPaused
            }, pauseTime)
            // æ¸…理定时器
            todoList._pauseTimer = pauseTimer
        }
    }
}
const getRandomColor = () => {
    // ç”Ÿæˆæµ…色:R、G、B åˆ†é‡éƒ½åœ¨ 150-255 ä¹‹é—´
    const r = Math.floor(Math.random() * 106) + 150; // 150-255
    const g = Math.floor(Math.random() * 106) + 150; // 150-255
    const b = Math.floor(Math.random() * 106) + 150; // 150-255
    // å°† RGB è½¬æ¢ä¸ºåå…­è¿›åˆ¶é¢œè‰²
    return '#' + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0');
}
// æ›´æ–°æ—¶é—´
const updateTime = () => {
  const now = new Date()
  currentTime.value = now.toLocaleTimeString('zh-CN', { hour12: false })
  currentDate.value = now.toLocaleDateString('zh-CN', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    weekday: 'long'
  })
}
// åˆå§‹åŒ–æ—¶é—´
const initTime = () => {
  updateTime()
  timer.value = setInterval(updateTime, 1000)
}
// å…¨å±åŠŸèƒ½å®žçŽ° - é’ˆå¯¹scale-container元素
const toggleFullscreen = () => {
    const element = document.querySelector('.scale-container')
    if (!element) return
    if (!isFullscreen.value) {
        if (element.requestFullscreen) {
            element.requestFullscreen()
        } else if (element.webkitRequestFullscreen) {
            element.webkitRequestFullscreen()
        } else if (element.msRequestFullscreen) {
            element.msRequestFullscreen()
        }
    } else {
        if (document.exitFullscreen) {
            document.exitFullscreen()
        } else if (document.webkitExitFullscreen) {
            document.webkitExitFullscreen()
        } else if (document.msExitFullscreen) {
            document.msExitFullscreen()
        }
    }
  const element = document.querySelector('.scale-container')
  if (!element) return
  if (!isFullscreen.value) {
    if (element.requestFullscreen) {
      element.requestFullscreen()
    } else if (element.webkitRequestFullscreen) {
      element.webkitRequestFullscreen()
    } else if (element.msRequestFullscreen) {
      element.msRequestFullscreen()
    }
  } else {
    if (document.exitFullscreen) {
      document.exitFullscreen()
    } else if (document.webkitExitFullscreen) {
      document.webkitExitFullscreen()
    } else if (document.msExitFullscreen) {
      document.msExitFullscreen()
    }
  }
}
// ç›‘听全屏变化事件
@@ -1303,7 +125,7 @@
                           document.webkitFullscreenElement || 
                           document.msFullscreenElement
  isFullscreen.value = fullscreenElement && fullscreenElement.classList.contains('scale-container')
  // å…¨å±çŠ¶æ€å˜åŒ–æ—¶ï¼Œå»¶è¿Ÿé‡æ–°è®¡ç®—ç¼©æ”¾æ¯”ä¾‹ï¼ˆç¡®ä¿DOM更新完成)
  setTimeout(() => {
    calculateScale()
@@ -1312,146 +134,19 @@
// ç”Ÿå‘½å‘¨æœŸé’©å­
onMounted(() => {
  initTime()
  // ä½¿ç”¨nextTick确保DOM完全渲染后再初始化图表
  // ä½¿ç”¨nextTick确保DOM完全渲染后再初始化
  nextTick(() => {
    // è®¡ç®—初始缩放比例
    calculateScale()
    // åˆå§‹åŒ–autofit自适应(如果需要保留autofit,可以保留,但主要缩放由scale-container控制)
    // autofit.init({ dh: 800, dw: 1280, el: '.data-dashboard', resize: true }, false)
    // æ·»åŠ è‡ªåŠ¨æ»šåŠ¨åŠ¨ç”»æ•ˆæžœ - å®¢æˆ·ä¿¡æ¯åˆ—表
    const contractList = refContractList.value
    if (contractList && contractList.scrollHeight > contractList.clientHeight) {
      // åˆ›å»ºä¸€ä¸ªå…‹éš†é¡¹ï¼Œç”¨äºŽå®žçŽ°æ— ç¼æ»šåŠ¨
      const scrollItems = Array.from(contractList.querySelectorAll('li'))
      const itemHeight = scrollItems[0]?.offsetHeight || 0
      const containerHeight = contractList.clientHeight
      const cloneCount = Math.ceil(containerHeight / itemHeight) + 2
      // å…‹éš†å‰å‡ ä¸ªé¡¹ç›®å¹¶æ·»åŠ åˆ°åˆ—è¡¨æœ«å°¾ï¼Œå®žçŽ°æ— ç¼æ»šåŠ¨
      for (let i = 0; i < cloneCount; i++) {
        const clone = scrollItems[i % scrollItems.length].cloneNode(true)
        contractList.appendChild(clone)
      }
      let scrollPosition = 0
      const scrollSpeed = 1.5 // å¢žåŠ æ»šåŠ¨é€Ÿåº¦ï¼Œä½¿æ»šåŠ¨æ›´åŠ æ˜Žæ˜¾
      const pauseTime = 3000 // æ»šåŠ¨æš‚åœæ—¶é—´
      let isPaused = false
      let lastTimestamp = 0
      // è¿žç»­æ»šåŠ¨åŠ¨ç”»å‡½æ•°
      function scrollAnimation(timestamp) {
        if (!lastTimestamp) lastTimestamp = timestamp
        const deltaTime = timestamp - lastTimestamp
        lastTimestamp = timestamp
        if (!isPaused) {
          scrollPosition += scrollSpeed * (deltaTime / 16) // æ ‡å‡†åŒ–为60fps的速度
          // å½“滚动超过原始内容长度时,重置位置实现无缝滚动
          if (scrollPosition >= contractList.scrollHeight - containerHeight - cloneCount * itemHeight) {
            scrollPosition = 0
            contractList.scrollTop = 0
          } else {
            contractList.scrollTop = scrollPosition
          }
        }
        timerScroll.value = requestAnimationFrame(scrollAnimation)
      }
      // å¯åŠ¨æ»šåŠ¨åŠ¨ç”»
      timerScroll.value = requestAnimationFrame(scrollAnimation)
      // è®¾ç½®æ»šåЍ-暂停-滚动的循环效果
      const pauseTimer = setInterval(() => {
        isPaused = !isPaused
      }, pauseTime)
      // æ¸…理定时器
      contractList._pauseTimer = pauseTimer
    }
    // å¾…办事项列表滚动功能已移至todoInfoS函数中,在获取数据后初始化
  })
  window.addEventListener('resize', handleResize)
  window.addEventListener('fullscreenchange', handleFullscreenChange)
  window.addEventListener('webkitfullscreenchange', handleFullscreenChange)
  window.addEventListener('MSFullscreenChange', handleFullscreenChange)
  analysisCustomer()
  workInProcessTurnoverInfo()
  qualityStatisticsInfo()
    // accountStatisticsInfo()
    progressStatisticsInfo()
  getNum()
  getLedgerNum()
  todoInfoS()
    statisticsReceivable()
    getAmountHalfYearNum()
  // è®¾ç½®è‡ªåŠ¨è½®æ¢å‘¨ã€æœˆã€å­£åº¦çš„å®šæ—¶å™¨ï¼Œæ¯10秒切换一次
  autoSwitchTimer.value = setInterval(() => {
    // å¾ªçŽ¯åˆ‡æ¢ï¼š1(周) -> 2(月) -> 3(季度) -> 1(周)
    radio1.value = radio1.value === 3 ? 1 : radio1.value + 1
    statisticsReceivable()
  }, 10000) // 10秒切换一次
})
onBeforeUnmount(() => {
  if (timer.value) {
    clearInterval(timer.value)
  }
  if (timerScroll.value) {
    cancelAnimationFrame(timerScroll.value)
  }
  // æ¸…理滚动列表的暂停定时器
  const contractList = refContractList.value
  if (contractList && contractList._pauseTimer) {
    clearInterval(contractList._pauseTimer)
  }
  // æ¸…理待办事项列表的动画和定时器
  const todoList = refTodoList.value
  if (todoList) {
    if (todoList._animationFrame) {
      cancelAnimationFrame(todoList._animationFrame)
      todoList._animationFrame = null
    }
    if (todoList._pauseTimer) {
      clearInterval(todoList._pauseTimer)
      todoList._pauseTimer = null
    }
  }
  // æ¸…理生产订单进度表格的动画和定时器
  const progressTable = progressTableRef.value
  if (progressTable) {
    if (progressTableScrollTimer.value) {
      cancelAnimationFrame(progressTableScrollTimer.value)
      progressTableScrollTimer.value = null
    }
    if (progressTable._pauseTimer) {
      clearInterval(progressTable._pauseTimer)
      progressTable._pauseTimer = null
    }
  }
  // æ¸…理表格滚动定时器
  if (tableScrollTimeout.value) {
    clearTimeout(tableScrollTimeout.value)
    tableScrollTimeout.value = null
  }
  // æ¸…理自动轮换周、月、季度的定时器
  if (autoSwitchTimer.value) {
    clearInterval(autoSwitchTimer.value)
    autoSwitchTimer.value = null
  }
  window.removeEventListener('resize', handleResize)
  window.removeEventListener('fullscreenchange', handleFullscreenChange)
  window.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
@@ -1461,7 +156,6 @@
    window.removeEventListener('resize', window._autofitUpdateHandler)
    delete window._autofitUpdateHandler
  }
  disposeCharts()
  // å…³é—­autofit
  autofit.off()
})
@@ -1470,74 +164,74 @@
<style scoped>
/* å¤–部缩放容器 - å æ®æ•´ä¸ªè§†å£ */
.scale-container {
  position: relative;
    width: 100%;
    /* é¡µé¢åœ¨å¸¸è§„布局下(有顶栏)默认减去 84px,避免内容被裁切 */
    height: calc(100vh - 84px);
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #000;
    overflow: hidden;
position: relative;
width: 100%;
/* é¡µé¢åœ¨å¸¸è§„布局下(有顶栏)默认减去 84px,避免内容被裁切 */
height: calc(100vh - 84px);
display: flex;
align-items: center;
justify-content: center;
background-color: #000;
overflow: hidden;
}
/* å†…部内容区域 - å›ºå®šè®¾è®¡å°ºå¯¸ */
.data-dashboard {
  position: relative;
  width: 1920px;
  height: 1080px;
    background-image: url("@/assets/BI/backImage@2x.png");
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    transform-origin: center center;
position: relative;
width: 1920px;
height: 1080px;
background-image: url("@/assets/BI/backImage@2x.png");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
transform-origin: center center;
}
/* å…¨å±çŠ¶æ€çš„æ ·å¼ - ä½œç”¨äºŽscale-container */
.scale-container:fullscreen {
    width: 100vw;
    height: 100vh;
  margin: 0;
  padding: 0;
  background-color: #000;
  z-index: 9999;
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
background-color: #000;
z-index: 9999;
}
/* Webkit浏览器前缀 */
.scale-container:-webkit-full-screen {
  width: 100vw;
  height: 100vh;
  margin: 0;
  padding: 0;
  background-color: #000;
  z-index: 9999;
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
background-color: #000;
z-index: 9999;
}
/* MS浏览器前缀 */
.scale-container:-ms-fullscreen {
  width: 100vw;
  height: 100vh;
  margin: 0;
  padding: 0;
  background-color: #000;
  z-index: 9999;
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
background-color: #000;
z-index: 9999;
}
.dashboard-header {
  position: relative;
  z-index: 1;
    height: 86px;
    background-image: url("@/assets/BI/biaoti.png");
    background-size: cover;
    background-repeat: no-repeat;
  display: flex;
  align-items: center;
  justify-content: center;
position: relative;
z-index: 1;
height: 86px;
background-image: url("@/assets/BI/biaoti.png");
background-size: cover;
background-repeat: no-repeat;
display: flex;
align-items: center;
justify-content: center;
}
.factory-name {
  font-weight: 600;
font-weight: 600;
font-size: 52px;
color: #FFFFFF;
top: 16px;
@@ -1545,492 +239,57 @@
}
.fullscreen-btn {
  position: absolute;
  top: 10px;
  left: 20px;
  width: 40px;
  height: 40px;
  background: rgba(0, 20, 60, 0.8);
  border: 1px solid rgba(0, 212, 255, 0.3);
  border-radius: 6px;
  color: #00d4ff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.3s;
  z-index: 10000;
position: absolute;
top: 10px;
left: 20px;
width: 40px;
height: 40px;
background: rgba(0, 20, 60, 0.8);
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 6px;
color: #00d4ff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
z-index: 10000;
}
.fullscreen-btn:hover {
  background: rgba(0, 30, 90, 0.9);
  border-color: rgba(0, 212, 255, 0.5);
background: rgba(0, 30, 90, 0.9);
border-color: rgba(0, 212, 255, 0.5);
}
.dashboard-content {
  position: relative;
  z-index: 1;
  display: flex;
  gap: 30px;
  padding: 0 30px;
    height: calc(100% - 86px);
  overflow: hidden;
position: relative;
z-index: 1;
display: flex;
gap: 30px;
padding: 0 30px;
height: calc(100% - 86px);
overflow: hidden;
}
/* ç¡®ä¿å„面板能够正确显示 */
.left-panel, .center-panel, .right-panel {
  overflow: hidden;
overflow: hidden;
}
.left-panel,
.right-panel {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 24px;
    width: 520px;
flex: 1;
display: flex;
flex-direction: column;
gap: 24px;
width: 520px;
}
.center-panel {
  flex: 1.5;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.panel-item-customers {
    border: 1px solid #1A58B0;
    padding: 18px;
    width: 100%;
    height: 540px;
}
.panel-title-second {
    height: 60px;
    display: flex;
    gap: 12px;
    margin-bottom: 20px;
    align-items: center;
}
.quality-cards {
    display: flex;
    gap: 12px;
    width: 100%;
    height: 54px;
    justify-content: space-between;
    align-items: center;
}
.quality-cardSec {
    display: flex;
}
.quality-cardTitle {
    font-weight: 400;
    font-size: 14px;
    color: #FFFFFF;
    display: flex;
    align-items: flex-start;
    flex-direction: column;
}
.quality-card {
    width: 80px;
    height: 60px;
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
}
.quality-card.one {
    background-image: url("@/assets/BI/yuancailiaoyijianicon@2x.png");
}
.quality-card.two {
    background-image: url("@/assets/BI/guochengyijianicon@2x.png");
}
.quality-card.three {
    background-image: url("@/assets/BI/chuchangyijianicon@2x.png");
flex: 1.5;
display: flex;
flex-direction: column;
gap: 20px;
}
/* è®¢å•统计卡片样式 */
.order-statistics-cards {
    display: flex;
    gap: 12px;
    width: 100%;
    height: 94px;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
}
.quality-card.four {
    background-image: url("@/assets/BI/yuancailiaoyijianicon@2x.png");
}
.quality-card.five {
    background-image: url("@/assets/BI/guochengyijianicon@2x.png");
}
.quality-card.six {
    background-image: url("@/assets/BI/chuchangyijianicon@2x.png");
}
.quality-card.seven {
    background-image: url("@/assets/BI/yuancailiaoyijianicon@2x.png");
}
.panel-title-icon {
    width: 60px;
    height: 60px;
    background-image: url("@/assets/BI/hetongicon.png");
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
}
.panel-header {
    background-image: url("@/assets/BI/kehuhetongback@2x.png");
    background-size: 100% 100%;
    background-position: center;
    background-repeat: no-repeat;
}
.panel-title {
    width: 100%;
    font-weight: 500;
    font-size: 16px;
    color: #D9ECFF;
    padding-left: 46px;
    line-height: 36px;
}
.total-customers {
    background-image: url("@/assets/BI/hetongjineback@2x.png");
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    width: 90%;
    height: 60px;
    display: flex;
    align-items: center;
    padding: 0 20px;
    gap: 20px;
}
.total-customers .label {
    font-weight: 500;
    font-size: 16px;
    color: #FFFFFF;
}
.total-customers .value {
    font-weight: 500;
    font-size: 40px;
    background: linear-gradient(360deg, #008BFD 0%, #FFFFFF 100%);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
}
.contract-list {
    margin-top: 16px;
    font-size: 14px;
    color: #666;
    list-style: none;
    padding: 0;
    height: 82%;
    overflow-y: auto;
    width: 460px;
    /* éšè—æ»šåŠ¨æ¡ä½†ä¿ç•™æ»šåŠ¨åŠŸèƒ½ */
    scrollbar-width: none; /* Firefox */
    -ms-overflow-style: none; /* IE和Edge */
}
/* Chrome、Safari和Opera */
.contract-list::-webkit-scrollbar {
    display: none;
}
.line {
    position: relative;
    width: 230px;
}
.line::after {
    content: '';
    position: absolute;
    right: 2px;
    top: 0;
    bottom: 0;
    width: 1px;
    background-color: #C9C5C5;
    border-radius: 2px;
}
.contract-list li {
    margin-top: 10px;
}
.stats-cards {
  display: flex;
  gap: 30px;
}
.stat-card {
  flex: 1;
  display: flex;
  align-items: center;
    background-image: url("@/assets/BI/border@2x.png");
    background-size: 100% 100%;
    background-position: center;
    background-repeat: no-repeat;
  height: 142px;
}
.card-icon {
  width: 100px;
  height: 100px;
  margin: 20px 20px 0 10px;
}
.card-content {
  display: flex;
  flex-direction: column;
    gap: 10px;
}
.card-value {
    font-weight: 500;
    font-size: 40px;
  background: linear-gradient(360deg, #008BFD 0%, #FFFFFF 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
.card-label {
    font-weight: 400;
    font-size: 19px;
    color: rgba(208,231,255,0.7);
}
.equipment-stats {
    border: 1px solid #1A58B0;
  padding: 18px;
  height: 240px;
}
.equipment-header {
    font-weight: 500;
    font-size: 21px;
    display: flex;
    border-bottom: 1px solid;
    border-image: linear-gradient( 270deg, rgba(0,126,255,0) 0%, rgba(0,126,255,0.4549) 35%, #007EFF 78%, #007EFF 100%) 1;
    padding-bottom: 2px;
}
.equipment-title {
    font-weight: 500;
    font-size: 21px;
    background: linear-gradient(360deg, #056DFF 0%, #43E8FC 100%);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    line-height: 50px;
}
.equipment-icon {
    width: 50px;
    height: 50px;
}
.equipment-items {
  display: flex;
  justify-content: space-around;
  gap: 30px;
}
.equipment-item {
  text-align: center;
}
.equipment-value {
  display: block;
    font-weight: 500;
    font-size: 40px;
    color: #FFFFFF;
    width: 120px;
    height: 110px;
    line-height: 110px;
    background-image: url("@/assets/BI/shujutongji@2x.png");
    background-size: 100% 100%;
    background-position: center;
    background-repeat: no-repeat;
  margin-bottom: 8px;
}
.equipment-label {
    font-weight: 500;
    font-size: 21px;
    color: #FFFFFE;
}
.event-info {
    background-image: url("@/assets/BI/shijianmingchengbeijing@2x.png");
    background-size: 100% 100%;
    background-position: center;
    background-repeat: no-repeat;
  padding: 20px;
  height: 186px;
}
.event-header {
    display: flex;
    align-items: center;
}
.event-icon {
    width: 40px;
    height: 40px;
}
.event-title {
    font-weight: 500;
    font-size: 24px;
    color: #FFFFFE;
    line-height: 30px;
}
.todo-list {
  list-style: none;
  padding: 0;
  margin: 0;
  height: 120px; /* æŒ‰ç”¨æˆ·è¦æ±‚调整高度 */
  overflow: hidden;
  font-size: 15px;
}
.todo-list li {
    border-radius: 8px;
    margin-bottom: 12px;
    padding: 12px 40px;
    height: 74px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.todo-title {
    font-weight: 400;
    font-size: 20px;
    color: #FFFFFE;
    position: relative;
}
.todo-title::before {
    content: ''; /* å¿…需,表示这里有一个内容 */
    position: absolute;
    left: -10px; /* å®šä½åˆ°å·¦ä¾§ */
    top: 50%; /* åž‚直居中 */
    transform: translateY(-50%); /* å¾®è°ƒåž‚直居中 */
    width: 6px; /* åœ†çš„直径 */
    height: 6px; /* åœ†çš„直径 */
    background: #498CEB;
    border-radius: 50%; /* è®©å…¶å˜æˆåœ†å½¢ */
}
.todo-division {
    font-weight: 400;
    font-size: 20px;
    color: #FFFFFE;
}
.todo-time {
    font-weight: 400;
    font-size: 20px;
    color: #FFFFFE;
}
.financial-header {
    background-image: url("@/assets/BI/caiwufenxiback@2x.png");
    background-size: 100% 100%;
    background-position: center;
    background-repeat: no-repeat;
}
.financial-title {
    width: 100%;
    font-weight: 500;
    font-size: 16px;
    color: #D9ECFF;
    padding-left: 46px;
    line-height: 36px;
}
/* è‡ªå®šä¹‰å•选按钮组样式 */
.custom-radio-group :deep(.el-radio-button__inner) {
  background-color: transparent;
  color: white;
  border-color: rgba(255, 255, 255, 0.3);
}
.custom-radio-group :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
  background-color: rgba(255, 255, 255, 0.2);
  color: white;
  border-color: rgba(255, 255, 255, 0.5);
  box-shadow: -1px 0 0 0 rgba(255, 255, 255, 0.5);
}
/* ç”Ÿäº§è®¢å•进度表格样式 */
.progress-table-container {
  height: 200px;
  overflow-y: auto;
  overflow-x: hidden;
  margin-top: 10px;
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* IE和Edge */
}
.progress-table-container::-webkit-scrollbar {
  display: none; /* Chrome、Safari和Opera */
}
.progress-table {
  width: 100%;
  border-collapse: collapse;
  color: #B8C8E0;
  font-size: 12px;
  table-layout: fixed;
}
.progress-table thead {
  position: sticky;
  top: 0;
  background-color: rgba(26, 88, 176, 0.9);
  z-index: 10;
}
.progress-table th {
  padding: 8px 6px;
  text-align: left;
  font-weight: 500;
  border-bottom: 1px solid rgba(184, 200, 224, 0.3);
  color: #B8C8E0;
  font-size: 12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.progress-table th:nth-child(1) { width: 15%; } /* ç”Ÿäº§è®¢å•号 */
.progress-table th:nth-child(2) { width: 15%; } /* äº§å“åç§° */
.progress-table th:nth-child(3) { width: 15%; } /* è§„æ ¼ */
.progress-table th:nth-child(4) { width: 12%; } /* éœ€æ±‚数量 */
.progress-table th:nth-child(5) { width: 12%; } /* å®Œæˆæ•°é‡ */
.progress-table th:nth-child(6) { width: 31%; } /* å®Œæˆè¿›åº¦ */
.progress-table td {
  padding: 8px 6px;
  border-bottom: 1px solid rgba(184, 200, 224, 0.1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 12px;
  transition: opacity 0.3s ease;
}
.progress-table tbody tr:hover {
  background-color: rgba(184, 200, 224, 0.1);
}
.progress-table tbody tr.row-under-header {
  opacity: 0.5;
}
/* el-progress ç»„件样式调整 */
.progress-table :deep(.el-progress) {
  width: 100%;
}
.progress-table :deep(.el-progress-bar__outer) {
  background-color: rgba(184, 200, 224, 0.2);
}
.progress-table :deep(.el-progress__text) {
  color: #B8C8E0;
  font-size: 11px;
}
</style>
src/views/reportAnalysis/dataDashboard/index0.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2036 @@
<template>
        <div class="scale-container">
            <div class="data-dashboard" :style="{ transform: `scale(${scaleRatio})` }">
      <!-- å…¨å±æŒ‰é’® - ç§»åŠ¨åˆ°å·¦ä¸Šè§’ -->
      <button class="fullscreen-btn" @click="toggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏显示'">
        <svg v-if="!isFullscreen" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
          <path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>
        </svg>
        <svg v-else width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
          <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
        </svg>
      </button>
      <!-- é¡¶éƒ¨æ ‡é¢˜æ  -->
      <div class="dashboard-header">
        <div class="factory-name">{{ userStore.currentFactoryName }}</div>
      </div>
      <!-- ä¸»è¦å†…容区域 -->
      <div class="dashboard-content">
      <!-- å·¦ä¾§åŒºåŸŸ -->
      <div class="left-panel">
        <!-- å®¢æˆ·ä¿¡æ¯ç»Ÿè®¡åˆ†æž -->
                <div class="panel-header">
                    <span class="panel-title">在制品统计分析</span>
                </div>
        <div class="panel-item-customers">
                    <div class="quality-cards">
                        <div class="quality-cardSec">
                            <div class="quality-card one"></div>
                            <div class="quality-cardTitle">
                                <div>总在制数量</div>
                                <div>{{workInProcessStatistics.totalQuantity}}ä»¶</div>
                            </div>
                        </div>
                        <div class="quality-cardSec">
                            <div class="quality-card two"></div>
                            <div class="quality-cardTitle">
                                <div>平均周转天数</div>
                                <div>{{workInProcessStatistics.avgTurnoverDays}}天</div>
                            </div>
                        </div>
                        <div class="quality-cardSec">
                            <div class="quality-card three"></div>
                            <div class="quality-cardTitle">
                                <div>周转效率</div>
                                <div>{{workInProcessStatistics.turnoverEfficiency}}%</div>
                            </div>
                        </div>
                    </div>
                    <!-- å·¥åºåœ¨åˆ¶å“æ•°é‡æŸ±çж图 -->
                    <div style="height: 70%">
                        <Echarts ref="chart"
                                         :chartStyle="chartStyle"
                                         :grid="grid"
                                         :legend="workInProcessBarLegend"
                                         :series="workInProcessBarSeries"
                                         :tooltip="tooltip"
                                         :xAxis="workInProcessXAxis"
                                         :yAxis="workInProcessYAxis"
                                         :options="{backgroundColor: 'transparent', textStyle: {color: '#B8C8E0'}}"
                                         style="height: 100%"></Echarts>
                    </div>
        </div>
        <!-- è´¨é‡ç»Ÿè®¡ -->
                <div class="panel-header">
                    <span class="panel-title">近4月质量统计</span>
                </div>
                <div class="main-panel">
                    <div class="panel-item-customers">
                        <div class="quality-cards">
                            <div class="quality-cardSec">
                                <div class="quality-card one"></div>
                                <div class="quality-cardTitle">
                                    <div>原材料检数</div>
                                    <div>{{qualityStatisticsObject.supplierNum}}ä»¶</div>
                                </div>
                            </div>
                            <div class="quality-cardSec">
                                <div class="quality-card two"></div>
                                <div class="quality-cardTitle">
                                    <div>过程检数</div>
                                    <div>{{qualityStatisticsObject.processNum}}ä»¶</div>
                                </div>
                            </div>
                            <div class="quality-cardSec">
                                <div class="quality-card three"></div>
                                <div class="quality-cardTitle">
                                    <div>出厂检数</div>
                                    <div>{{qualityStatisticsObject.factoryNum}}ä»¶</div>
                                </div>
                            </div>
                        </div>
                        <Echarts ref="chart"
                                         :chartStyle="chartStyle"
                                         :grid="grid"
                                         :legend="barLegend"
                                         :series="barSeries1"
                                         :tooltip="tooltip"
                                         :xAxis="xAxis1"
                                         :yAxis="yAxis1"
                                         :options="{backgroundColor: 'transparent', textStyle: {color: '#B8C8E0'}}"
                                         style="height: 260px"></Echarts>
                    </div>
                </div>
      </div>
      <!-- ä¸­é—´åŒºåŸŸ -->
      <div class="center-panel">
        <!-- é¡¶éƒ¨ç»Ÿè®¡å¡ç‰‡ -->
        <div class="stats-cards">
          <div class="stat-card">
            <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
            <div class="card-content">
              <span class="card-label">员工总数</span>
              <span class="card-value">{{totalStaff}}</span>
            </div>
          </div>
          <div class="stat-card">
            <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
            <div class="card-content">
              <span class="card-label">客户总数</span>
              <span class="card-value">{{totalCustomers}}</span>
            </div>
          </div>
          <div class="stat-card">
            <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
            <div class="card-content">
              <span class="card-label">供应商总数</span>
              <span class="card-value">{{totalSuppliers}}</span>
            </div>
          </div>
        </div>
        <!-- è®¾å¤‡ç»Ÿè®¡ -->
        <div class="equipment-stats">
          <div class="equipment-header">
                        <img src="@/assets/BI/shujutongjiicon@2x.png" alt="图标" class="equipment-icon" />
            <span class="equipment-title">设备统计</span>
          </div>
          <div class="equipment-items">
            <div class="equipment-item">
              <span class="equipment-value">{{equipmentNum}}</span>
              <span class="equipment-label">设备总数</span>
            </div>
            <div class="equipment-item">
              <span class="equipment-value">{{equipmentRepair}}</span>
              <span class="equipment-label">待维修设备</span>
            </div>
            <div class="equipment-item">
              <span class="equipment-value">{{equipmentMaintain}}</span>
              <span class="equipment-label">待保养设备</span>
            </div>
            <div class="equipment-item">
              <span class="equipment-value">{{totalMeasuring}}</span>
              <span class="equipment-label">计量器具总数</span>
            </div>
          </div>
        </div>
        <!-- äº‹ä»¶åç§° -->
        <div class="event-info">
          <div class="event-header">
                        <img src="@/assets/BI/shijianmingxiicon@2x.png" alt="图标" class="event-icon" />
            <span class="event-title">事件名称</span>
          </div>
          <div class="event-content">
                        <ul class="todo-list" v-if="todoList.length > 0" ref="refTodoList">
   <li v-for="item in todoList" :key="item.id">
    <div style="display: flex;flex-direction: column;justify-content: space-between;width: 100%;gap: 20px">
     <div style="display: flex;justify-content: space-between;align-items: center;">
      <div class="todo-title">待办编号:{{item.approveId}}</div>
      <div class="todo-division">部门:{{item.approveDeptName}}</div>
      <div class="todo-time">{{item.approveTime}}</div>
     </div>
     <div class="todo-division">待办事由:{{item.approveReason}}</div>
    </div>
   </li>
 </ul>
                        <div v-else style="text-align: center">
                            æš‚无数据
                        </div>
          </div>
        </div>
                <div class="financial-header">
                    <span class="financial-title">各生产订单的完成进度统计</span>
                </div>
                <div class="main-panel">
                    <div class="panel-item-customers">
                        <div class="order-statistics-cards" style="margin-bottom: 0px;">
                            <div class="quality-cardSec">
                                <div class="quality-card four"></div>
                                <div class="quality-cardTitle">
                                    <div>总订单数</div>
                                    <div>{{orderStatisticsObject.totalOrderCount}}ä»¶</div>
                                </div>
                            </div>
                            <div class="quality-cardSec">
                                <div class="quality-card five"></div>
                                <div class="quality-cardTitle">
                                    <div>未完成订单数</div>
                                    <div>{{orderStatisticsObject.uncompletedOrderCount}}ä»¶</div>
                                </div>
                            </div>
                            <div class="quality-cardSec">
                                <div class="quality-card six"></div>
                                <div class="quality-cardTitle">
                                    <div>部分完成订单数</div>
                                    <div>{{orderStatisticsObject.partialCompletedOrderCount}}ä»¶</div>
                                </div>
                            </div>
                            <div class="quality-cardSec">
                                <div class="quality-card seven"></div>
                                <div class="quality-cardTitle">
                                    <div>已完成订单数</div>
                                    <div>{{orderStatisticsObject.completedOrderCount}}ä»¶</div>
                                </div>
                            </div>
                        </div>
                        <div class="progress-table-container" ref="progressTableRef" style="margin-top: 0px;" @scroll="handleTableScroll">
                            <table class="progress-table">
                                <thead>
                                    <tr>
                                        <th>生产订单号</th>
                                        <th>产品名称</th>
                                        <th>规格</th>
                                        <th>需求数量</th>
                                        <th>完成数量</th>
                                        <th>完成进度</th>
                                    </tr>
                                </thead>
                                <tbody>
                                    <tr
                                        v-for="(item, index) in progressTableData"
                                        :key="index"
                                        :ref="el => setRowRef(el, index)"
                                        :class="{ 'row-under-header': isRowUnderHeader(index) }"
                                    >
                                        <td>{{ item.npsNo || '-' }}</td>
                                        <td>{{ item.productCategory || '-' }}</td>
                                        <td>{{ item.specificationModel || '-' }}</td>
                                        <td>{{ item.quantity || 0 }}</td>
                                        <td>{{ item.completeQuantity || 0 }}</td>
                                        <td>
                                            <el-progress
                                                :percentage="calculateProgress(item)"
                                                :color="progressColor(calculateProgress(item))"
                                                :status="calculateProgress(item) >= 100 ? 'success' : ''"
                                                :stroke-width="8"
                                            />
                                        </td>
                                    </tr>
                                </tbody>
                            </table>
                        </div>
                    </div>
                </div>
      </div>
      <!-- å³ä¾§åŒºåŸŸ -->
      <div class="right-panel">
        <!-- åº”收应付统计 -->
                <div class="panel-header">
                    <span class="panel-title">应收应付统计</span>
                </div>
                <div class="panel-item-customers">
                    <div style="display: flex;justify-content: space-between;margin-bottom: 20px;">
                        <div class="section-title">应收应付统计</div>
<!--                        <el-radio-group v-model="radio1" size="large" @change="statisticsReceivable" class="custom-radio-group">-->
<!--                            <el-radio-button label="按周" :value="1" />-->
<!--                            <el-radio-button label="按月" :value="2" />-->
<!--                            <el-radio-button label="按季度" :value="3" />-->
<!--                        </el-radio-group>-->
                    </div>
                    <Echarts ref="chart"
                                     :color="barColors2"
                                     :chartStyle="chartStyle"
                                     :grid="grid"
                   :legend="barLegend2"
                                     :series="barSeries"
                                     :tooltip="tooltip"
                                     :xAxis="xAxis"
                                     :yAxis="yAxis"
                                     :options="{backgroundColor: 'transparent', textStyle: {color: '#B8C8E0'}}"
                                     style="height: 260px"></Echarts>
                </div>
        <!-- å›žæ¬¾ä¸Žå¼€ç¥¨åˆ†æž -->
         <div class="panel-header">
                    <span class="panel-title">近一月回款与开票分析</span>
                </div>
        <div class="panel-item-customers" style="padding-top: 60px;">
                    <Echarts ref="chart" :chartStyle="chartStyle" :grid="grid" :legend="lineLegend" :series="lineSeries"
                                 :tooltip="tooltipLine" :xAxis="xAxis2" :yAxis="yAxis2" :options="{backgroundColor: 'transparent', textStyle: {color: '#FFFFFF'}}" style="height: 270px;"></Echarts>
                </div>
      </div>
      </div>
      </div>
    </div>
</template>
<script setup>
import * as echarts from 'echarts'
import { ref, reactive, onMounted, onBeforeUnmount, nextTick } from 'vue'
import autofit from 'autofit.js'
import Echarts from "@/components/Echarts/echarts.vue";
import useUserStore from '@/store/modules/user'
import {
    analysisCustomerContractAmounts, getAmountHalfYear,
    homeTodos,
    qualityStatistics,
    statisticsReceivablePayable,
    getProgressStatistics,
      getWorkInProcessTurnover
} from "@/api/viewIndex.js";
import {staffOnJobListPage} from "@/api/personnelManagement/employeeRecord.js";
import {listCustomer} from "@/api/basicData/customerFile.js";
import {listSupplier} from "@/api/basicData/supplierManageFile.js";
import {getLedgerPage} from "@/api/equipmentManagement/ledger.js";
import {getRepairPage} from "@/api/equipmentManagement/repair.js";
import {getUpkeepPage} from "@/api/equipmentManagement/upkeep.js";
import {measuringInstrumentListPage} from "@/api/equipmentManagement/measurementEquipment.js";
import {listPageAnalysis} from "@/api/financialManagement/expenseManagement.js";
import {productOrderListPage} from "@/api/productionManagement/productionOrder.js";
// å…¨å±ç›¸å…³çŠ¶æ€
const isFullscreen = ref(false);
// ç¼©æ”¾æ¯”例
const scaleRatio = ref(1)
// è®¾è®¡å°ºå¯¸ï¼ˆåŸºå‡†å°ºå¯¸ï¼‰- æ ¹æ®å®žé™…设计稿调整
const designWidth = 1920
const designHeight = 1080
// ç”¨æˆ·store
const userStore = useUserStore()
// å“åº”式数据
const currentTime = ref('')
const currentDate = ref('')
const timer = ref(null)
const charts = ref([])
// å›¾è¡¨å¼•用
const customerPieChartRef = ref(null)
const salesBarChartRef = ref(null)
const dataBarChartRef = ref(null)
const financialAreaChartRef = ref(null)
const realtimeLineChartRef = ref(null)
const refContractList = ref(null)
const refTodoList = ref(null)
const progressTableRef = ref(null)
const timerScroll = ref(null)
const progressTableScrollTimer = ref(null)
const isTableScrolling = ref(false)
const tableScrollTimeout = ref(null)
const tableRowRefs = ref([])
const rowsUnderHeader = ref(new Set())
const chartStylePie = {
    width: '100%',
    height: '100%' // è®¾ç½®å›¾è¡¨å®¹å™¨çš„高度
}
const materialPieSeries = ref([
    {
        type: 'pie',
        radius: ['0%', '90%'],
        avoidLabelOverlap: false,
        itemStyle: {
            borderColor: '#fff',
            borderWidth: 0
        },
        label: {
            show: false
        },
        data: []
    }
])
const pieLegend = reactive({
    show: false,
})
const sum = ref(0)
const totalStaff = ref(0)
const totalCustomers = ref(0)
const totalSuppliers = ref(0)
const yny = ref(0)
const chain = ref(0)
const equipmentNum = ref(0)
const equipmentRepair = ref(0)
const equipmentMaintain = ref(0)
const totalMeasuring = ref(0)
const pieTooltip = reactive({
    trigger: 'item',
    formatter: function (params) {
        // åŠ¨æ€ç”Ÿæˆæç¤ºä¿¡æ¯ï¼ŒåŸºäºŽæ•°æ®é¡¹çš„ name å±žæ€§
        const description = params.name === '本月回款金额' ? '本月回款金额' : '应收款金额';
        return `<div style="color: #B8C8E0">${description} ${params.value}元 ${params.percent}%</div>`;
    },
    position: 'right'
})
const qualityStatisticsObject = ref({
    supplierNum: 0,
    processNum: 0,
    factoryNum: 0,
})
// è®¢å•统计对象
const orderStatisticsObject = ref({
    totalOrderCount: 0,
    uncompletedOrderCount: 0,
    partialCompletedOrderCount: 0,
    completedOrderCount: 0,
})
// åœ¨åˆ¶å“å‘¨è½¬ç»Ÿè®¡å¯¹è±¡
const workInProcessStatistics = ref({
    totalQuantity: 0,
    avgTurnoverDays: 0,
    turnoverEfficiency: 0,
})
const chartStyle = {
    width: '100%',
    height: '150%' // è®¾ç½®å›¾è¡¨å®¹å™¨çš„高度
}
const barSeries = ref([
    {
        name: '应付金额',
        type: 'bar',
        data: [],
        label: {
            show: true,
        },
        itemStyle: {
            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                { offset: 0, color: '#00A4ED' },
                { offset: 1, color: '#4EE4FF' }
            ])
        }
    },
    {
        name: '应收金额',
        type: 'bar',
        data: [],
        label: {
            show: true,
        },
        itemStyle: {
            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                { offset: 0, color: '#537EF5' },
                { offset: 1, color: '#9061F8' }
            ])
        }
    }
])
const radio1 = ref(1)
const barColors2 = ['#5181DB', '#D369E0', '#F2CA6D', '#60CCA8']
const grid = {
    left: '3%',
    right: '4%',
    bottom: '3%',
    containLabel: true
}
const lineLegend = {
    show: true,
  textStyle: { color: '#B8C8E0' },
    data: ['开票', '回款']
}
const lineSeries = ref([
    {
        type: 'line',
        data: [],
        label: {
            show: true
        },
        showSymbol: true, // æ˜¾ç¤ºåœ†ç‚¹
    },
])
const tooltipLine = {
    trigger: 'axis',
}
const yAxis2 = ref([
    {
        type: 'value',
    }
])
const xAxis2 = ref([
    {
        type: 'category',
        data: [],
        axisLabel: {
            interval: 0,
            formatter: function(value) {
                return value.replace(/~/g, '\n');
            },
        }
    }
])
const barLegend2 = {
    show: true,
    textStyle: { color: '#B8C8E0' },
    data: ['应付金额', '应收金额']
}
const barLegend = {
    show: true,
    textStyle: { color: '#B8C8E0' },
    data: ['原材料合格数', '过程合格数', '出不合格数']
}
const barLegend1 = {
    show: false,
    textStyle: { color: '#B8C8E0' },
    data: []
}
const barSeries11 = ref([
    {
        name: '生产订单统计',
        type: 'bar',
        barGap: 0,
        emphasis: {
            focus: 'series'
        },
        itemStyle: {
            // ä½¿ç”¨å‡½æ•°æ ¹æ®æ•°æ®ç´¢å¼•返回不同颜色
            color: function(params) {
                const colorStops = [
                    [
                        { offset: 1, color: '#00A4ED' },
                        { offset: 0, color: '#4EE4FF' }
                    ],
                    [
                        { offset: 1, color: '#3378FF' },
                        { offset: 0, color: '#4E8AFF' }
                    ],
                    [
                        { offset: 1, color: '#FF6B6B' },
                        { offset: 0, color: '#FF8E8E' }
                    ],
                    [
                        { offset: 1, color: '#537EF5' },
                        { offset: 0, color: '#9061F8' }
                    ]
                ]
                const stops = colorStops[params.dataIndex] || colorStops[0]
                return {
                    type: 'linear',
                    x: 0,
                    y: 0,
                    x2: 0,
                    y2: 1,
                    colorStops: stops
                }
            }
        },
        data: []
    }
])
const barSeries1 = ref([
    {
        name: '原材料合格数',
        type: 'bar',
        barGap: 0,
        emphasis: {
            focus: 'series'
        },
        itemStyle: {
            color: {
                type: 'linear',
                x: 0,
                y: 0,
                x2: 0,
                y2: 1,
                colorStops: [
                    { offset: 1, color: '#00A4ED' },
                    { offset: 0, color: '#4EE4FF' }
                ]
            }
        },
        data: []
    },
    {
        name: '过程合格数',
        type: 'bar',
        emphasis: {
            focus: 'series'
        },
        itemStyle: {
            color: {
                type: 'linear',
                x: 0,
                y: 0,
                x2: 0,
                y2: 1,
                colorStops: [
                    { offset: 1, color: '#3378FF' },
                    { offset: 0, color: '#4E8AFF' }
                ]
            }
        },
        data: []
    },
    {
        name: '出厂合格数',
        type: 'bar',
        emphasis: {
            focus: 'series'
        },
        itemStyle: {
            color: {
                type: 'linear',
                x: 0,
                y: 0,
                x2: 0,
                y2: 1,
                colorStops: [
                    { offset: 1, color: '#537EF5' },
                    { offset: 0, color: '#9061F8' }
                ]
            }
        },
        data: []
    },
])
const tooltip = {
    trigger: 'axis',
    axisPointer: {
        type: 'shadow'
    },
    formatter: function (params) {
        let result = params[0].axisValueLabel + '<br/>';
        params.forEach(item => {
            result += `<div style="color: #B8C8E0">${item.marker} ${item.seriesName}: ${item.value}</div>`;
        });
        return result;
    }
}
const xAxis = [{
    type: 'value',
}]
const yAxis = [{
    type: 'category',
    data: ['应收应付统计']
}]
const xAxis1 = ref([{
    type: 'category',
    axisTick: { show: false },
    axisLabel: { color: '#B8C8E0' },
    data: []
}])
const yAxis1 = [{
    type: 'value',
    axisLabel: { color: '#B8C8E0' }
}]
const xAxis3 = ref([{
    type: 'category',
    axisTick: { show: false },
    axisLabel: { color: '#B8C8E0' },
    data: []
}])
const yAxis3 = [{
    type: 'value',
    axisLabel: { color: '#B8C8E0' }
}]
// åœ¨åˆ¶å“å·¥åºæŸ±çŠ¶å›¾é…ç½®
const workInProcessXAxis = ref([{
    type: 'category',
    axisTick: { show: false },
    axisLabel: { color: '#B8C8E0' },
    data: []
}])
const workInProcessYAxis = [{
    type: 'value',
    axisLabel: { color: '#B8C8E0' },
    name: ''
}]
const workInProcessBarLegend = {
    show: false,
    textStyle: { color: '#B8C8E0' },
    data: []
}
const workInProcessBarSeries = ref([
    {
        name: '在制品数量',
        type: 'bar',
        barWidth: 25, // å›ºå®šæŸ±çŠ¶å›¾å®½åº¦ä¸º40px
        barGap: 0,
        emphasis: {
            focus: 'series'
        },
        itemStyle: {
            color: {
                type: 'linear',
                x: 0,
                y: 0,
                x2: 0,
                y2: 1,
                colorStops: [
                    { offset: 0, color: '#4EE4FF' },
                    { offset: 1, color: '#00A4ED' }
                ]
            }
        },
        label: {
            show: true,
            position: 'top',
            color: '#B8C8E0'
        },
        data: []
    }
])
// å¾…办事项
const todoList = ref([])
// ç”Ÿäº§è®¢å•完成进度表格数据
const progressTableData = ref([])
// è®¡ç®—完成进度百分比
const calculateProgress = (item) => {
    if (!item) return 0
    // ä¼˜å…ˆä½¿ç”¨completionStatus字段
    if (item.completionStatus !== undefined && item.completionStatus !== null) {
        const percentage = Number(item.completionStatus)
        if (isNaN(percentage)) return 0
        return Math.min(Math.max(Math.round(percentage), 0), 100)
    }
    // å¦‚果没有completionStatus,则根据完成数量和需求数量计算
    if (!item.quantity || item.quantity === 0) return 0
    const percentage = (item.completeQuantity || 0) / item.quantity * 100
    return Math.min(Math.max(Math.round(percentage), 0), 100)
}
// æ ¹æ®è¿›åº¦ç™¾åˆ†æ¯”返回颜色
const progressColor = (percentage) => {
    const p = percentage || 0
    if (p < 30) return "#f56c6c"
    if (p < 50) return "#e6a23c"
    if (p < 80) return "#409eff"
    return "#67c23a"
}
// è®¡ç®—缩放比例
const calculateScale = () => {
  const container = document.querySelector('.scale-container')
  if (!container) return
  // èŽ·å–å®¹å™¨çš„å®žé™…å°ºå¯¸
    const rect = container.getBoundingClientRect?.()
    const containerWidth = container.clientWidth || rect?.width || window.innerWidth
    const containerHeight = container.clientHeight || rect?.height || window.innerHeight
  // è®¡ç®—宽高缩放比例,取较小值以保证内容完整显示(等比缩放)
  const scaleX = containerWidth / designWidth
  const scaleY = containerHeight / designHeight
  scaleRatio.value = Math.min(scaleX, scaleY)
  // è§¦å‘图表resize
  charts.value.forEach(chart => {
    if (chart && chart.resize) {
      chart.resize()
    }
  })
}
// çª—口大小变化处理
const handleResize = () => {
  // å»¶è¿Ÿæ‰§è¡Œï¼Œç¡®ä¿DOM更新完成
  setTimeout(() => {
    calculateScale()
  }, 100)
}
// é”€æ¯å›¾è¡¨å®žä¾‹
const disposeCharts = () => {
  charts.value.forEach(chart => {
    if (chart && chart.dispose) {
      chart.dispose()
    }
  })
  charts.value = []
}
// åˆåŒé‡‘额
const analysisCustomer = () => {
    analysisCustomerContractAmounts().then((res) => {
        sum.value = res.data.sum
        yny.value = res.data.yny
        chain.value = res.data.chain
        // ä¸ºæ¯ä¸ªæ•°æ®é¡¹åˆ†é…éšæœºé¢œè‰²
        materialPieSeries.value[0].data = res.data.item.map(item => ({
            ...item,
            itemStyle: { color: getRandomColor() }
        }))
    })
}
// åœ¨åˆ¶å“å‘¨è½¬ç»Ÿè®¡
const workInProcessTurnoverInfo = () => {
    getWorkInProcessTurnover().then((res) => {
        console.log("在制品周转统计数据:", res)
        if (!res || !res.data) {
            console.warn('在制品周转统计数据为空')
            return
        }
        // ä»ŽæŽ¥å£èŽ·å–ç»Ÿè®¡æ•°æ®
        workInProcessStatistics.value = {
            totalQuantity: res.data.totalOrderCount || 0,
            avgTurnoverDays: res.data.averageTurnoverDays || 0,
            turnoverEfficiency: res.data.turnoverEfficiency || 0,
        }
        // è®¾ç½®å·¥åºæŸ±çŠ¶å›¾æ•°æ®
        // X轴:processDetails (工序详情数组)
        // Y轴:processQuantityDetails (工序数量详情数组)
        if (res.data.processDetails && Array.isArray(res.data.processDetails)) {
            // è®¾ç½®X轴数据(工序名称)
            workInProcessXAxis.value[0].data = res.data.processDetails
        } else {
            workInProcessXAxis.value[0].data = []
        }
        if (res.data.processQuantityDetails && Array.isArray(res.data.processQuantityDetails)) {
            // è®¾ç½®Y轴数据(在制品数量)
            workInProcessBarSeries.value[0].data = res.data.processQuantityDetails
        } else {
            workInProcessBarSeries.value[0].data = []
        }
    }).catch((error) => {
        console.error('获取在制品周转统计失败:', error)
    })
}
// è´¨æ£€ç»Ÿè®¡
const qualityStatisticsInfo = () => {
    qualityStatistics().then((res) => {
        res.data.item.forEach(item => {
            xAxis1.value[0].data.push(item.date)
            barSeries1.value[0].data.push(item.supplierNum)
            barSeries1.value[1].data.push(item.processNum)
            barSeries1.value[2].data.push(item.factoryNum)
        })
        qualityStatisticsObject.value.supplierNum = res.data.supplierNum
        qualityStatisticsObject.value.processNum = res.data.processNum
        qualityStatisticsObject.value.factoryNum = res.data.factoryNum
    })
}
// å„生产订单的完成进度统计
const progressStatisticsInfo = () => {
    // ä»Žç»Ÿè®¡æŽ¥å£èŽ·å–ç»Ÿè®¡æ•°æ®
    getProgressStatistics().then((res) => {
        console.log("生产订单完成进度统计数据:", res)
        if (!res || !res.data) {
            console.warn('生产订单完成进度统计数据为空')
            return
        }
        // ä»ŽæŽ¥å£èŽ·å–ç»Ÿè®¡æ•°æ®
        orderStatisticsObject.value = {
            totalOrderCount: res.data.totalOrderCount || 0,
            uncompletedOrderCount: res.data.uncompletedOrderCount || 0,
            partialCompletedOrderCount: res.data.partialCompletedOrderCount || 0,
            completedOrderCount: res.data.completedOrderCount || 0
        }
        progressTableData.value = res.data.completedOrderDetails || []
        // é‡ç½®è¡Œå¼•用
        tableRowRefs.value = []
        rowsUnderHeader.value.clear()
        // åœ¨èŽ·å–åˆ°æ•°æ®åŽï¼Œåˆå§‹åŒ–æ»šåŠ¨åŠŸèƒ½
        nextTick(() => {
            initProgressTableScroll()
        })
    }).catch((error) => {
        console.error('获取生产订单完成进度统计失败:', error)
    })
}
// è´¢åŠ¡ç»Ÿè®¡
// const accountStatisticsInfo = () => {
//     listPageAnalysis().then((res) => {
//         xAxis3.value[0].data = res.data.days
//         barSeries11.value[0].data = res.data.totalIncome
//     })
// }
const getNum = () => {
    const params = {
        pageNum: -1,
        pageSize: -1,
    }
    staffOnJobListPage({...params, staffState: 1}).then(res => {
        totalStaff.value = res.data.total
    })
    listCustomer(params).then((res) => {
        totalCustomers.value = res.total;
    });
    listSupplier(params).then((res) => {
        totalSuppliers.value = res.data.total
    });
}
const getLedgerNum = () => {
    const params = {
        pageNum: -1,
        pageSize: -1,
    }
    getLedgerPage(params).then((res) => {
        equipmentNum.value = res.data.total
    });
    getRepairPage({...params, status:0}).then((res) => {
        equipmentRepair.value = res.data.total
    });
    getUpkeepPage({...params, status:0}).then((res) => {
        equipmentMaintain.value = res.data.total
    });
    measuringInstrumentListPage(params).then((res) => {
        totalMeasuring.value = res.data.total
    });
}
// å¾…办事项
const todoInfoS = () => {
    homeTodos().then((res) => {
        todoList.value = res.data
        // åœ¨èŽ·å–åˆ°å¾…åŠžäº‹é¡¹æ•°æ®åŽï¼Œåˆå§‹åŒ–æ»šåŠ¨åŠŸèƒ½
        nextTick(() => {
            initTodoListScroll()
        })
    })
}
// åº”付应收统计
const statisticsReceivable = (type) => {
    statisticsReceivablePayable({type: radio1.value}).then((res) => {
        // è®¾ç½®åº”付金额数据
        barSeries.value[0].data = [
            { value: res.data.payableMoney }
        ]
        // è®¾ç½®åº”收金额数据
        barSeries.value[1].data = [
            { value: res.data.receivableMoney }
        ]
    })
}
const getAmountHalfYearNum = async () => {
    const res = await getAmountHalfYear()
    console.log(res)
    const monthName = []
    const receiptAmount = []
    const invoiceAmount = []
    res.data.forEach(item => {
        monthName.push(item.month)
        receiptAmount.push(item.receiptAmount)
        invoiceAmount.push(item.invoiceAmount)
    })
    // æ­£ç¡®å“åº”式赋值:创建新的 xAxis å’Œ series å¯¹è±¡
    xAxis2.value[0].data = monthName
    xAxis2.value[0].data = monthName.map(item => item.replace(/~/g, '\n~'));
    lineSeries.value = [
        {
            name: '开票',
            type: 'line',
            data: receiptAmount,
            stack: 'Total',
            areaStyle: {
                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                    {
                        offset: 0,
                        color: 'rgba(131, 207, 255, 1)'
                    },
                    {
                        offset: 1,
                        color: 'rgba(186, 228, 255, 1)'
                    }
                ])
            },
            itemStyle: {
                color: '#2D99FF',
                borderColor: '#2D99FF'
            },
            emphasis: {
                focus: 'series'
            },
            lineStyle: {
                width: 0
            },
            showSymbol: true,
        },
        {
            name: '回款',
            type: 'line',
            data: invoiceAmount,
            stack: 'Total',
            lineStyle: {
                width: 0
            },
            itemStyle: {
                color: '#83CFFF',
                borderColor: '#83CFFF'
            },
            showSymbol: true,
            areaStyle: {
                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                    {
                        offset: 0,
                        color: 'rgba(54, 153, 255, 1)'
                    },
                    {
                        offset: 1,
                        color: 'rgba(89, 169, 254, 1)'
                    }
                ])
            },
            emphasis: {
                focus: 'series'
            },
        }
    ]
}
// è‡ªåŠ¨è½®æ¢å‘¨ã€æœˆã€å­£åº¦çš„å®šæ—¶å™¨
const autoSwitchTimer = ref(null)
// è®¾ç½®è¡Œå¼•用
const setRowRef = (el, index) => {
    if (el) {
        tableRowRefs.value[index] = el
    }
}
// åˆ¤æ–­è¡Œæ˜¯å¦åœ¨è¡¨å¤´ä¸‹æ–¹
const isRowUnderHeader = (index) => {
    return rowsUnderHeader.value.has(index)
}
// å¤„理表格滚动事件
const handleTableScroll = () => {
    const tableContainer = progressTableRef.value
    if (!tableContainer) return
    const thead = tableContainer.querySelector('thead')
    if (!thead) return
    const theadHeight = thead.offsetHeight
    const containerRect = tableContainer.getBoundingClientRect()
    const containerTop = containerRect.top
    const theadBottom = containerTop + theadHeight
    // æ¸…空之前的记录
    rowsUnderHeader.value.clear()
    // æ£€æŸ¥æ¯ä¸€è¡Œæ˜¯å¦åœ¨è¡¨å¤´ä¸‹æ–¹ï¼ˆè¢«è¡¨å¤´é®æŒ¡ï¼‰
    tableRowRefs.value.forEach((row, index) => {
        if (row) {
            const rowRect = row.getBoundingClientRect()
            const rowTop = rowRect.top
            const rowBottom = rowRect.bottom
            // å¦‚果行与表头有重叠(行在表头下方被遮挡)
            // è¡Œçš„顶部在表头底部下方,但行的底部在表头底部上方,说明被遮挡
            if (rowTop < theadBottom && rowBottom > containerTop) {
                rowsUnderHeader.value.add(index)
            }
        }
    })
    // æ¸…除之前的定时器
    if (tableScrollTimeout.value) {
        clearTimeout(tableScrollTimeout.value)
    }
    // æ»šåŠ¨åœæ­¢åŽæ¸…ç©ºæ·¡åŒ–æ ‡è®°
    tableScrollTimeout.value = setTimeout(() => {
        rowsUnderHeader.value.clear()
    }, 150)
}
// åˆå§‹åŒ–生产订单进度表格滚动功能
const initProgressTableScroll = () => {
    const tableContainer = progressTableRef.value
    if (!tableContainer) return
    // æ¸…理之前的滚动动画和定时器
    if (progressTableScrollTimer.value) {
        cancelAnimationFrame(progressTableScrollTimer.value)
        progressTableScrollTimer.value = null
    }
    if (tableContainer._pauseTimer) {
        clearInterval(tableContainer._pauseTimer)
        tableContainer._pauseTimer = null
    }
    const tbody = tableContainer.querySelector('tbody')
    if (!tbody) return
    // æ¸…理之前可能存在的克隆行(保留原始数据行)
    // åŽŸå§‹æ•°æ®è¡Œçš„æ•°é‡åº”è¯¥ç­‰äºŽ progressTableData.value.length
    const originalCount = progressTableData.value.length
    const allRows = Array.from(tbody.querySelectorAll('tr'))
    if (allRows.length > originalCount) {
        // ç§»é™¤æ‰€æœ‰è¶…过原始数量的行(这些是克隆的行)
        for (let i = originalCount; i < allRows.length; i++) {
            allRows[i].remove()
        }
    }
    const scrollItems = Array.from(tbody.querySelectorAll('tr'))
    if (scrollItems.length === 0) return
    // èŽ·å–åŽŸå§‹æ•°æ®é¡¹æ•°é‡
    const originalItemCount = scrollItems.length
    // è®¡ç®—容器高度和表头高度
    const thead = tableContainer.querySelector('thead')
    const theadHeight = thead ? thead.offsetHeight : 40
    const containerHeight = tableContainer.clientHeight
    const visibleHeight = containerHeight - theadHeight
    // è®¡ç®—原始数据的总高度
    const itemHeight = scrollItems[0]?.offsetHeight || 40
    const totalContentHeight = itemHeight * originalItemCount
    // å¦‚果数据量不够,容器可以完全显示所有数据,就不需要滚动和克隆
    if (totalContentHeight <= visibleHeight) {
        // æ•°æ®é‡å°‘,不需要滚动,直接返回
        return
    }
    // æ•°æ®é‡è¶³å¤Ÿï¼Œéœ€è¦æ»šåŠ¨ï¼Œè¿›è¡Œå…‹éš†ä»¥å®žçŽ°æ— ç¼æ»šåŠ¨
    const cloneCount = Math.ceil(visibleHeight / itemHeight) + 2
    // å…‹éš†å‰å‡ ä¸ªé¡¹ç›®å¹¶æ·»åŠ åˆ°åˆ—è¡¨æœ«å°¾ï¼Œå®žçŽ°æ— ç¼æ»šåŠ¨
    for (let i = 0; i < cloneCount; i++) {
        const clone = scrollItems[i % originalItemCount].cloneNode(true)
        tbody.appendChild(clone)
    }
    let scrollPosition = 0
    const scrollSpeed = 1.5
    const pauseTime = 3000
    let isPaused = false
    let lastTimestamp = 0
    // è¿žç»­æ»šåŠ¨åŠ¨ç”»å‡½æ•°
    function scrollAnimation(timestamp) {
        if (!lastTimestamp) lastTimestamp = timestamp
        const deltaTime = timestamp - lastTimestamp
        lastTimestamp = timestamp
        if (!isPaused) {
            scrollPosition += scrollSpeed * (deltaTime / 16)
            // è®¡ç®—最大滚动位置(原始内容的高度)
            const maxScroll = itemHeight * originalItemCount
            // å½“滚动超过原始内容长度时,重置位置实现无缝滚动
            if (scrollPosition >= maxScroll) {
                scrollPosition = 0
                tableContainer.scrollTop = 0
            } else {
                tableContainer.scrollTop = scrollPosition
            }
        }
        progressTableScrollTimer.value = requestAnimationFrame(scrollAnimation)
    }
    // å¯åŠ¨æ»šåŠ¨åŠ¨ç”»
    progressTableScrollTimer.value = requestAnimationFrame(scrollAnimation)
    // è®¾ç½®æ»šåЍ-暂停-滚动的循环效果
    const pauseTimer = setInterval(() => {
        isPaused = !isPaused
    }, pauseTime)
    // æ¸…理定时器
    tableContainer._pauseTimer = pauseTimer
}
// åˆå§‹åŒ–待办事项列表滚动功能
const initTodoListScroll = () => {
    const todoList = refTodoList.value
    // å¼ºåˆ¶å¯ç”¨æ»šåŠ¨ï¼Œä¸æ£€æŸ¥ä»»ä½•æ¡ä»¶
    if (todoList) {
        // åˆ›å»ºä¸€ä¸ªå…‹éš†é¡¹ï¼Œç”¨äºŽå®žçŽ°æ— ç¼æ»šåŠ¨
        const scrollItems = Array.from(todoList.querySelectorAll('li'))
        if (scrollItems.length > 0) {
            // ç¡®ä¿æœ‰è¶³å¤Ÿçš„项目用于滚动
            // å¦‚果项目太少,多复制几次以确保滚动效果
            if (scrollItems.length < 4) {
                const originalItems = [...scrollItems]
                for (let i = 0; i < 4; i++) {
                    originalItems.forEach(item => {
                        const clone = item.cloneNode(true)
                        todoList.appendChild(clone)
                    })
                }
                // é‡æ–°èŽ·å–æ‰€æœ‰é¡¹ç›®
                scrollItems.push(...Array.from(todoList.querySelectorAll('li')).slice(scrollItems.length));
            }
            const itemHeight = scrollItems[0]?.offsetHeight || 0
            const containerHeight = todoList.clientHeight
            const cloneCount = Math.ceil(containerHeight / itemHeight) + 2
            // å…‹éš†å‰å‡ ä¸ªé¡¹ç›®å¹¶æ·»åŠ åˆ°åˆ—è¡¨æœ«å°¾ï¼Œå®žçŽ°æ— ç¼æ»šåŠ¨
            for (let i = 0; i < cloneCount; i++) {
                const clone = scrollItems[i % scrollItems.length].cloneNode(true)
                todoList.appendChild(clone)
            }
            let scrollPosition = 0
            const scrollSpeed = 1.5 // å¢žåŠ æ»šåŠ¨é€Ÿåº¦ï¼Œä½¿æ»šåŠ¨æ›´åŠ æ˜Žæ˜¾
            const pauseTime = 3000 // æ»šåŠ¨æš‚åœæ—¶é—´
            let isPaused = false
            let lastTimestamp = 0
            // è¿žç»­æ»šåŠ¨åŠ¨ç”»å‡½æ•°
            function scrollAnimation(timestamp) {
                if (!lastTimestamp) lastTimestamp = timestamp
                const deltaTime = timestamp - lastTimestamp
                lastTimestamp = timestamp
                if (!isPaused) {
                    scrollPosition += scrollSpeed * (deltaTime / 16) // æ ‡å‡†åŒ–为60fps的速度
                    // å½“滚动超过原始内容长度时,重置位置实现无缝滚动
                    const maxScroll = Math.max(todoList.scrollHeight - containerHeight - cloneCount * itemHeight, itemHeight * scrollItems.length)
                    if (scrollPosition >= maxScroll) {
                        scrollPosition = 0
                        todoList.scrollTop = 0
                    } else {
                        todoList.scrollTop = scrollPosition
                    }
                }
                todoList._animationFrame = requestAnimationFrame(scrollAnimation)
            }
            // å¯åŠ¨æ»šåŠ¨åŠ¨ç”»
            todoList._animationFrame = requestAnimationFrame(scrollAnimation)
            // è®¾ç½®æ»šåЍ-暂停-滚动的循环效果
            const pauseTimer = setInterval(() => {
                isPaused = !isPaused
            }, pauseTime)
            // æ¸…理定时器
            todoList._pauseTimer = pauseTimer
        }
    }
}
const getRandomColor = () => {
    // ç”Ÿæˆæµ…色:R、G、B åˆ†é‡éƒ½åœ¨ 150-255 ä¹‹é—´
    const r = Math.floor(Math.random() * 106) + 150; // 150-255
    const g = Math.floor(Math.random() * 106) + 150; // 150-255
    const b = Math.floor(Math.random() * 106) + 150; // 150-255
    // å°† RGB è½¬æ¢ä¸ºåå…­è¿›åˆ¶é¢œè‰²
    return '#' + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0');
}
// æ›´æ–°æ—¶é—´
const updateTime = () => {
  const now = new Date()
  currentTime.value = now.toLocaleTimeString('zh-CN', { hour12: false })
  currentDate.value = now.toLocaleDateString('zh-CN', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    weekday: 'long'
  })
}
// åˆå§‹åŒ–æ—¶é—´
const initTime = () => {
  updateTime()
  timer.value = setInterval(updateTime, 1000)
}
// å…¨å±åŠŸèƒ½å®žçŽ° - é’ˆå¯¹scale-container元素
const toggleFullscreen = () => {
    const element = document.querySelector('.scale-container')
    if (!element) return
    if (!isFullscreen.value) {
        if (element.requestFullscreen) {
            element.requestFullscreen()
        } else if (element.webkitRequestFullscreen) {
            element.webkitRequestFullscreen()
        } else if (element.msRequestFullscreen) {
            element.msRequestFullscreen()
        }
    } else {
        if (document.exitFullscreen) {
            document.exitFullscreen()
        } else if (document.webkitExitFullscreen) {
            document.webkitExitFullscreen()
        } else if (document.msExitFullscreen) {
            document.msExitFullscreen()
        }
    }
}
// ç›‘听全屏变化事件
const handleFullscreenChange = () => {
  const fullscreenElement = document.fullscreenElement ||
                           document.webkitFullscreenElement ||
                           document.msFullscreenElement
  isFullscreen.value = fullscreenElement && fullscreenElement.classList.contains('scale-container')
  // å…¨å±çŠ¶æ€å˜åŒ–æ—¶ï¼Œå»¶è¿Ÿé‡æ–°è®¡ç®—ç¼©æ”¾æ¯”ä¾‹ï¼ˆç¡®ä¿DOM更新完成)
  setTimeout(() => {
    calculateScale()
  }, 200)
}
// ç”Ÿå‘½å‘¨æœŸé’©å­
onMounted(() => {
  initTime()
  // ä½¿ç”¨nextTick确保DOM完全渲染后再初始化图表
  nextTick(() => {
    // è®¡ç®—初始缩放比例
    calculateScale()
    // åˆå§‹åŒ–autofit自适应(如果需要保留autofit,可以保留,但主要缩放由scale-container控制)
    // autofit.init({ dh: 800, dw: 1280, el: '.data-dashboard', resize: true }, false)
    // æ·»åŠ è‡ªåŠ¨æ»šåŠ¨åŠ¨ç”»æ•ˆæžœ - å®¢æˆ·ä¿¡æ¯åˆ—表
    const contractList = refContractList.value
    if (contractList && contractList.scrollHeight > contractList.clientHeight) {
      // åˆ›å»ºä¸€ä¸ªå…‹éš†é¡¹ï¼Œç”¨äºŽå®žçŽ°æ— ç¼æ»šåŠ¨
      const scrollItems = Array.from(contractList.querySelectorAll('li'))
      const itemHeight = scrollItems[0]?.offsetHeight || 0
      const containerHeight = contractList.clientHeight
      const cloneCount = Math.ceil(containerHeight / itemHeight) + 2
      // å…‹éš†å‰å‡ ä¸ªé¡¹ç›®å¹¶æ·»åŠ åˆ°åˆ—è¡¨æœ«å°¾ï¼Œå®žçŽ°æ— ç¼æ»šåŠ¨
      for (let i = 0; i < cloneCount; i++) {
        const clone = scrollItems[i % scrollItems.length].cloneNode(true)
        contractList.appendChild(clone)
      }
      let scrollPosition = 0
      const scrollSpeed = 1.5 // å¢žåŠ æ»šåŠ¨é€Ÿåº¦ï¼Œä½¿æ»šåŠ¨æ›´åŠ æ˜Žæ˜¾
      const pauseTime = 3000 // æ»šåŠ¨æš‚åœæ—¶é—´
      let isPaused = false
      let lastTimestamp = 0
      // è¿žç»­æ»šåŠ¨åŠ¨ç”»å‡½æ•°
      function scrollAnimation(timestamp) {
        if (!lastTimestamp) lastTimestamp = timestamp
        const deltaTime = timestamp - lastTimestamp
        lastTimestamp = timestamp
        if (!isPaused) {
          scrollPosition += scrollSpeed * (deltaTime / 16) // æ ‡å‡†åŒ–为60fps的速度
          // å½“滚动超过原始内容长度时,重置位置实现无缝滚动
          if (scrollPosition >= contractList.scrollHeight - containerHeight - cloneCount * itemHeight) {
            scrollPosition = 0
            contractList.scrollTop = 0
          } else {
            contractList.scrollTop = scrollPosition
          }
        }
        timerScroll.value = requestAnimationFrame(scrollAnimation)
      }
      // å¯åŠ¨æ»šåŠ¨åŠ¨ç”»
      timerScroll.value = requestAnimationFrame(scrollAnimation)
      // è®¾ç½®æ»šåЍ-暂停-滚动的循环效果
      const pauseTimer = setInterval(() => {
        isPaused = !isPaused
      }, pauseTime)
      // æ¸…理定时器
      contractList._pauseTimer = pauseTimer
    }
    // å¾…办事项列表滚动功能已移至todoInfoS函数中,在获取数据后初始化
  })
  window.addEventListener('resize', handleResize)
  window.addEventListener('fullscreenchange', handleFullscreenChange)
  window.addEventListener('webkitfullscreenchange', handleFullscreenChange)
  window.addEventListener('MSFullscreenChange', handleFullscreenChange)
  analysisCustomer()
  workInProcessTurnoverInfo()
  qualityStatisticsInfo()
    // accountStatisticsInfo()
    progressStatisticsInfo()
  getNum()
  getLedgerNum()
  todoInfoS()
    statisticsReceivable()
    getAmountHalfYearNum()
  // è®¾ç½®è‡ªåŠ¨è½®æ¢å‘¨ã€æœˆã€å­£åº¦çš„å®šæ—¶å™¨ï¼Œæ¯10秒切换一次
  autoSwitchTimer.value = setInterval(() => {
    // å¾ªçŽ¯åˆ‡æ¢ï¼š1(周) -> 2(月) -> 3(季度) -> 1(周)
    radio1.value = radio1.value === 3 ? 1 : radio1.value + 1
    statisticsReceivable()
  }, 10000) // 10秒切换一次
})
onBeforeUnmount(() => {
  if (timer.value) {
    clearInterval(timer.value)
  }
  if (timerScroll.value) {
    cancelAnimationFrame(timerScroll.value)
  }
  // æ¸…理滚动列表的暂停定时器
  const contractList = refContractList.value
  if (contractList && contractList._pauseTimer) {
    clearInterval(contractList._pauseTimer)
  }
  // æ¸…理待办事项列表的动画和定时器
  const todoList = refTodoList.value
  if (todoList) {
    if (todoList._animationFrame) {
      cancelAnimationFrame(todoList._animationFrame)
      todoList._animationFrame = null
    }
    if (todoList._pauseTimer) {
      clearInterval(todoList._pauseTimer)
      todoList._pauseTimer = null
    }
  }
  // æ¸…理生产订单进度表格的动画和定时器
  const progressTable = progressTableRef.value
  if (progressTable) {
    if (progressTableScrollTimer.value) {
      cancelAnimationFrame(progressTableScrollTimer.value)
      progressTableScrollTimer.value = null
    }
    if (progressTable._pauseTimer) {
      clearInterval(progressTable._pauseTimer)
      progressTable._pauseTimer = null
    }
  }
  // æ¸…理表格滚动定时器
  if (tableScrollTimeout.value) {
    clearTimeout(tableScrollTimeout.value)
    tableScrollTimeout.value = null
  }
  // æ¸…理自动轮换周、月、季度的定时器
  if (autoSwitchTimer.value) {
    clearInterval(autoSwitchTimer.value)
    autoSwitchTimer.value = null
  }
  window.removeEventListener('resize', handleResize)
  window.removeEventListener('fullscreenchange', handleFullscreenChange)
  window.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
  window.removeEventListener('MSFullscreenChange', handleFullscreenChange)
  // ç§»é™¤æˆ‘们添加的autofit动态调整监听器
  if (window._autofitUpdateHandler) {
    window.removeEventListener('resize', window._autofitUpdateHandler)
    delete window._autofitUpdateHandler
  }
  disposeCharts()
  // å…³é—­autofit
  autofit.off()
})
</script>
<style scoped>
/* å¤–部缩放容器 - å æ®æ•´ä¸ªè§†å£ */
.scale-container {
  position: relative;
    width: 100%;
    /* é¡µé¢åœ¨å¸¸è§„布局下(有顶栏)默认减去 84px,避免内容被裁切 */
    height: calc(100vh - 84px);
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #000;
    overflow: hidden;
}
/* å†…部内容区域 - å›ºå®šè®¾è®¡å°ºå¯¸ */
.data-dashboard {
  position: relative;
  width: 1920px;
  height: 1080px;
    background-image: url("@/assets/BI/backImage@2x.png");
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    transform-origin: center center;
}
/* å…¨å±çŠ¶æ€çš„æ ·å¼ - ä½œç”¨äºŽscale-container */
.scale-container:fullscreen {
    width: 100vw;
    height: 100vh;
  margin: 0;
  padding: 0;
  background-color: #000;
  z-index: 9999;
}
/* Webkit浏览器前缀 */
.scale-container:-webkit-full-screen {
  width: 100vw;
  height: 100vh;
  margin: 0;
  padding: 0;
  background-color: #000;
  z-index: 9999;
}
/* MS浏览器前缀 */
.scale-container:-ms-fullscreen {
  width: 100vw;
  height: 100vh;
  margin: 0;
  padding: 0;
  background-color: #000;
  z-index: 9999;
}
.dashboard-header {
  position: relative;
  z-index: 1;
    height: 86px;
    background-image: url("@/assets/BI/biaoti.png");
    background-size: cover;
    background-repeat: no-repeat;
  display: flex;
  align-items: center;
  justify-content: center;
}
.factory-name {
  font-weight: 600;
font-size: 52px;
color: #FFFFFF;
top: 16px;
position: absolute;
}
.fullscreen-btn {
  position: absolute;
  top: 10px;
  left: 20px;
  width: 40px;
  height: 40px;
  background: rgba(0, 20, 60, 0.8);
  border: 1px solid rgba(0, 212, 255, 0.3);
  border-radius: 6px;
  color: #00d4ff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.3s;
  z-index: 10000;
}
.fullscreen-btn:hover {
  background: rgba(0, 30, 90, 0.9);
  border-color: rgba(0, 212, 255, 0.5);
}
.dashboard-content {
  position: relative;
  z-index: 1;
  display: flex;
  gap: 30px;
  padding: 0 30px;
    height: calc(100% - 86px);
  overflow: hidden;
}
/* ç¡®ä¿å„面板能够正确显示 */
.left-panel, .center-panel, .right-panel {
  overflow: hidden;
}
.left-panel,
.right-panel {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 24px;
    width: 520px;
}
.center-panel {
  flex: 1.5;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.panel-item-customers {
    border: 1px solid #1A58B0;
    padding: 18px;
    width: 100%;
    height: 540px;
}
.panel-title-second {
    height: 60px;
    display: flex;
    gap: 12px;
    margin-bottom: 20px;
    align-items: center;
}
.quality-cards {
    display: flex;
    gap: 12px;
    width: 100%;
    height: 54px;
    justify-content: space-between;
    align-items: center;
}
.quality-cardSec {
    display: flex;
}
.quality-cardTitle {
    font-weight: 400;
    font-size: 14px;
    color: #FFFFFF;
    display: flex;
    align-items: flex-start;
    flex-direction: column;
}
.quality-card {
    width: 80px;
    height: 60px;
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
}
.quality-card.one {
    background-image: url("@/assets/BI/yuancailiaoyijianicon@2x.png");
}
.quality-card.two {
    background-image: url("@/assets/BI/guochengyijianicon@2x.png");
}
.quality-card.three {
    background-image: url("@/assets/BI/chuchangyijianicon@2x.png");
}
/* è®¢å•统计卡片样式 */
.order-statistics-cards {
    display: flex;
    gap: 12px;
    width: 100%;
    height: 94px;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
}
.quality-card.four {
    background-image: url("@/assets/BI/yuancailiaoyijianicon@2x.png");
}
.quality-card.five {
    background-image: url("@/assets/BI/guochengyijianicon@2x.png");
}
.quality-card.six {
    background-image: url("@/assets/BI/chuchangyijianicon@2x.png");
}
.quality-card.seven {
    background-image: url("@/assets/BI/yuancailiaoyijianicon@2x.png");
}
.panel-title-icon {
    width: 60px;
    height: 60px;
    background-image: url("@/assets/BI/hetongicon.png");
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
}
.panel-header {
    background-image: url("@/assets/BI/kehuhetongback@2x.png");
    background-size: 100% 100%;
    background-position: center;
    background-repeat: no-repeat;
}
.panel-title {
    width: 100%;
    font-weight: 500;
    font-size: 16px;
    color: #D9ECFF;
    padding-left: 46px;
    line-height: 36px;
}
.total-customers {
    background-image: url("@/assets/BI/hetongjineback@2x.png");
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    width: 90%;
    height: 60px;
    display: flex;
    align-items: center;
    padding: 0 20px;
    gap: 20px;
}
.total-customers .label {
    font-weight: 500;
    font-size: 16px;
    color: #FFFFFF;
}
.total-customers .value {
    font-weight: 500;
    font-size: 40px;
    background: linear-gradient(360deg, #008BFD 0%, #FFFFFF 100%);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
}
.contract-list {
    margin-top: 16px;
    font-size: 14px;
    color: #666;
    list-style: none;
    padding: 0;
    height: 82%;
    overflow-y: auto;
    width: 460px;
    /* éšè—æ»šåŠ¨æ¡ä½†ä¿ç•™æ»šåŠ¨åŠŸèƒ½ */
    scrollbar-width: none; /* Firefox */
    -ms-overflow-style: none; /* IE和Edge */
}
/* Chrome、Safari和Opera */
.contract-list::-webkit-scrollbar {
    display: none;
}
.line {
    position: relative;
    width: 230px;
}
.line::after {
    content: '';
    position: absolute;
    right: 2px;
    top: 0;
    bottom: 0;
    width: 1px;
    background-color: #C9C5C5;
    border-radius: 2px;
}
.contract-list li {
    margin-top: 10px;
}
.stats-cards {
  display: flex;
  gap: 30px;
}
.stat-card {
  flex: 1;
  display: flex;
  align-items: center;
    background-image: url("@/assets/BI/border@2x.png");
    background-size: 100% 100%;
    background-position: center;
    background-repeat: no-repeat;
  height: 142px;
}
.card-icon {
  width: 100px;
  height: 100px;
  margin: 20px 20px 0 10px;
}
.card-content {
  display: flex;
  flex-direction: column;
    gap: 10px;
}
.card-value {
    font-weight: 500;
    font-size: 40px;
  background: linear-gradient(360deg, #008BFD 0%, #FFFFFF 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
.card-label {
    font-weight: 400;
    font-size: 19px;
    color: rgba(208,231,255,0.7);
}
.equipment-stats {
    border: 1px solid #1A58B0;
  padding: 18px;
  height: 240px;
}
.equipment-header {
    font-weight: 500;
    font-size: 21px;
    display: flex;
    border-bottom: 1px solid;
    border-image: linear-gradient( 270deg, rgba(0,126,255,0) 0%, rgba(0,126,255,0.4549) 35%, #007EFF 78%, #007EFF 100%) 1;
    padding-bottom: 2px;
}
.equipment-title {
    font-weight: 500;
    font-size: 21px;
    background: linear-gradient(360deg, #056DFF 0%, #43E8FC 100%);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    line-height: 50px;
}
.equipment-icon {
    width: 50px;
    height: 50px;
}
.equipment-items {
  display: flex;
  justify-content: space-around;
  gap: 30px;
}
.equipment-item {
  text-align: center;
}
.equipment-value {
  display: block;
    font-weight: 500;
    font-size: 40px;
    color: #FFFFFF;
    width: 120px;
    height: 110px;
    line-height: 110px;
    background-image: url("@/assets/BI/shujutongji@2x.png");
    background-size: 100% 100%;
    background-position: center;
    background-repeat: no-repeat;
  margin-bottom: 8px;
}
.equipment-label {
    font-weight: 500;
    font-size: 21px;
    color: #FFFFFE;
}
.event-info {
    background-image: url("@/assets/BI/shijianmingchengbeijing@2x.png");
    background-size: 100% 100%;
    background-position: center;
    background-repeat: no-repeat;
  padding: 20px;
  height: 186px;
}
.event-header {
    display: flex;
    align-items: center;
}
.event-icon {
    width: 40px;
    height: 40px;
}
.event-title {
    font-weight: 500;
    font-size: 24px;
    color: #FFFFFE;
    line-height: 30px;
}
.todo-list {
  list-style: none;
  padding: 0;
  margin: 0;
  height: 120px; /* æŒ‰ç”¨æˆ·è¦æ±‚调整高度 */
  overflow: hidden;
  font-size: 15px;
}
.todo-list li {
    border-radius: 8px;
    margin-bottom: 12px;
    padding: 12px 40px;
    height: 74px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.todo-title {
    font-weight: 400;
    font-size: 20px;
    color: #FFFFFE;
    position: relative;
}
.todo-title::before {
    content: ''; /* å¿…需,表示这里有一个内容 */
    position: absolute;
    left: -10px; /* å®šä½åˆ°å·¦ä¾§ */
    top: 50%; /* åž‚直居中 */
    transform: translateY(-50%); /* å¾®è°ƒåž‚直居中 */
    width: 6px; /* åœ†çš„直径 */
    height: 6px; /* åœ†çš„直径 */
    background: #498CEB;
    border-radius: 50%; /* è®©å…¶å˜æˆåœ†å½¢ */
}
.todo-division {
    font-weight: 400;
    font-size: 20px;
    color: #FFFFFE;
}
.todo-time {
    font-weight: 400;
    font-size: 20px;
    color: #FFFFFE;
}
.financial-header {
    background-image: url("@/assets/BI/caiwufenxiback@2x.png");
    background-size: 100% 100%;
    background-position: center;
    background-repeat: no-repeat;
}
.financial-title {
    width: 100%;
    font-weight: 500;
    font-size: 16px;
    color: #D9ECFF;
    padding-left: 46px;
    line-height: 36px;
}
/* è‡ªå®šä¹‰å•选按钮组样式 */
.custom-radio-group :deep(.el-radio-button__inner) {
  background-color: transparent;
  color: white;
  border-color: rgba(255, 255, 255, 0.3);
}
.custom-radio-group :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
  background-color: rgba(255, 255, 255, 0.2);
  color: white;
  border-color: rgba(255, 255, 255, 0.5);
  box-shadow: -1px 0 0 0 rgba(255, 255, 255, 0.5);
}
/* ç”Ÿäº§è®¢å•进度表格样式 */
.progress-table-container {
  height: 200px;
  overflow-y: auto;
  overflow-x: hidden;
  margin-top: 10px;
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* IE和Edge */
}
.progress-table-container::-webkit-scrollbar {
  display: none; /* Chrome、Safari和Opera */
}
.progress-table {
  width: 100%;
  border-collapse: collapse;
  color: #B8C8E0;
  font-size: 12px;
  table-layout: fixed;
}
.progress-table thead {
  position: sticky;
  top: 0;
  background-color: rgba(26, 88, 176, 0.9);
  z-index: 10;
}
.progress-table th {
  padding: 8px 6px;
  text-align: left;
  font-weight: 500;
  border-bottom: 1px solid rgba(184, 200, 224, 0.3);
  color: #B8C8E0;
  font-size: 12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.progress-table th:nth-child(1) { width: 15%; } /* ç”Ÿäº§è®¢å•号 */
.progress-table th:nth-child(2) { width: 15%; } /* äº§å“åç§° */
.progress-table th:nth-child(3) { width: 15%; } /* è§„æ ¼ */
.progress-table th:nth-child(4) { width: 12%; } /* éœ€æ±‚数量 */
.progress-table th:nth-child(5) { width: 12%; } /* å®Œæˆæ•°é‡ */
.progress-table th:nth-child(6) { width: 31%; } /* å®Œæˆè¿›åº¦ */
.progress-table td {
  padding: 8px 6px;
  border-bottom: 1px solid rgba(184, 200, 224, 0.1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 12px;
  transition: opacity 0.3s ease;
}
.progress-table tbody tr:hover {
  background-color: rgba(184, 200, 224, 0.1);
}
.progress-table tbody tr.row-under-header {
  opacity: 0.5;
}
/* el-progress ç»„件样式调整 */
.progress-table :deep(.el-progress) {
  width: 100%;
}
.progress-table :deep(.el-progress-bar__outer) {
  background-color: rgba(184, 200, 224, 0.2);
}
.progress-table :deep(.el-progress__text) {
  color: #B8C8E0;
  font-size: 11px;
}
</style>
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>
            </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>
@@ -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,31 +445,313 @@
})
</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>
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>
@@ -94,8 +96,8 @@
                  <el-button 
                    link 
                    type="primary" 
                    size="small"
                    :disabled="scope.row.approveStatus !== 1 || !!scope.row.shippingDate || !!scope.row.shippingCarNumber"
                    size="small"
                    :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-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="projectName">
              <el-input v-model="form.projectName" placeholder="请输入" clearable :disabled="operationType === 'view'" />
            </el-form-item>
          </el-col>
                    <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>
@@ -1211,6 +1211,8 @@
            // åŠ è½½å¤±è´¥æ—¶ä¿æŒå¯ç¼–è¾‘ï¼Œä¸ä¸­æ–­å¼¹çª—
            console.error("加载产品规格型号失败", e);
        }
    } else {
        getProductOptions()
    }
    productFormVisible.value = true;
};
@@ -1880,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 ä¸‹è½½æ–‡ä»¶çš„相关信息对象
@@ -1896,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: "货车",