zhangwencui
昨天 766ce6b9beedcc89ee83fe5daed0523bbd8c7e33
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-management into dev_New
已添加13个文件
已修改23个文件
7915 ■■■■■ 文件已修改
src/api/inventoryManagement/stockInventory.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockUninventory.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/procurementManagement/paymentLedger.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productWorkOrderFile.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/workOrder.js 10 ●●●●● 补丁 | 查看 | 原始文档 | 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/inventoryManagement/receiptManagement/Record.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/FrozenAndThaw.vue 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/New.vue 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Qualified.vue 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Subtract.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Unqualified.vue 45 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockReport/index.vue 364 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/invoiceEntry/components/Modal.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/paymentLedger/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementReport/index.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/Detail/index.vue 757 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionDispatching/components/formDia.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/components/formDia.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrder/components/filesDia.vue 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrder/index.vue 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue 28 ●●●● 补丁 | 查看 | 原始文档 | 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/api/inventoryManagement/stockInventory.js
@@ -41,3 +41,22 @@
        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/productionManagement/productWorkOrderFile.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
import request from "@/utils/request";
// æŸ¥è¯¢å·¥å•附件列表
export function productWorkOrderFileListPage(query) {
  return request({
    url: "/productWorkOrderFile/listPage",
    method: "get",
    params: query,
  });
}
// æ–°å¢žå·¥å•附件
export function productWorkOrderFileAdd(data) {
  return request({
    url: "/productWorkOrderFile/add",
    method: "post",
    data,
  });
}
// åˆ é™¤å·¥å•附件
export function productWorkOrderFileDel(data) {
  return request({
    url: "/productWorkOrderFile/del",
    method: "delete",
    data,
  });
}
src/api/productionManagement/workOrder.js
@@ -23,3 +23,13 @@
    data: data,
  });
}
// ä¸‹è½½å·¥å•流转卡(返回文件流)
export function downProductWorkOrder(id) {
  return request({
    url: "/productWorkOrder/down",
    method: "post",
    data: { id },
    responseType: "blob",
  });
}
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/inventoryManagement/receiptManagement/Record.vue
@@ -164,6 +164,7 @@
  const params = {...page, type: props.type};
  params.timeStr = searchForm.value.timeStr;
  params.productName = searchForm.value.productName;
  params.recordType = searchForm.value.recordType;
  getStockInRecordListPage(params)
      .then(res => {
        tableData.value = res.data.records;
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/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
@@ -27,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>
@@ -47,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>
@@ -64,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([])
@@ -78,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({
@@ -128,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) => {
  // è¿‡æ»¤æŽ‰å­æ•°æ®
@@ -138,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 '';
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
@@ -14,17 +14,17 @@
          <el-option label="月报" value="monthly" />
          <el-option label="进出存报表" value="inout" />
        </el-select>
        <span class="search_title ml10">时间范围:</span>
        <el-date-picker
          v-if="searchForm.reportType === 'daily'"
          v-model="searchForm.singleDate"
          type="date"
          placeholder="请选择日期"
          format="YYYY-MM-DD"
          value-format="YYYY-MM-DD"
          style="width: 200px;"
        />
         <el-date-picker
           v-if="searchForm.reportType === 'daily'"
           v-model="searchForm.singleDate"
           type="date"
           placeholder="请选择日期"
           format="YYYY-MM-DD"
           value-format="YYYY-MM-DD"
           style="width: 200px;"
         />
        <el-date-picker
          v-else-if="searchForm.reportType === 'monthly'"
          v-model="searchForm.monthRange"
@@ -47,7 +47,7 @@
          value-format="YYYY-MM-DD"
          style="width: 240px;"
        />
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æŸ¥è¯¢
        </el-button>
@@ -55,91 +55,91 @@
      </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>
    <!--    &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>-->
<!--    &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>-->
    <!--    &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>-->
<!--    &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">
@@ -147,88 +147,88 @@
        <template #header>
          <span>{{ getTableTitle() }}</span>
        </template>
        <el-table
            v-loading="tableLoading"
            :data="reportData.tableData"
            border
            height="400"
            style="width: 100%"
            :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
        >
         <el-table
           v-loading="tableLoading"
           :data="reportData.tableData"
           border
           height="400"
           style="width: 100%"
           :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
         >
          <el-table-column
              align="center"
              label="序号"
              type="index"
              width="60"
            align="center"
            label="序号"
            type="index"
            width="60"
          />
          <el-table-column
              label="入库时间"
              prop="createTime"
              width="200"
              show-overflow-tooltip
              v-if="searchForm.reportType !== 'inout'"
          />
          <el-table-column
              label="入库批次"
              prop="inboundBatches"
              width="240"
              show-overflow-tooltip
              v-if="searchForm.reportType !== 'inout'"
          />
          <el-table-column
              label="产品大类"
              prop="productName"
              show-overflow-tooltip
          />
          <el-table-column
              label="规格型号"
              prop="model"
              show-overflow-tooltip
          />
          <el-table-column
              label="单位"
              prop="unit"
              show-overflow-tooltip
          />
          <el-table-column
              label="入库数量"
              prop="totalStockIn"
              align="center"
              v-if="searchForm.reportType === 'inout'"
          />
          <el-table-column
              label="入库数量"
              prop="stockInNum"
              align="center"
              v-else
          />
          <el-table-column
              label="出库数量"
              prop="totalStockOut"
              width="100"
              align="center"
              v-if="searchForm.reportType === 'inout'"
          />
          <el-table-column
              label="现在库存"
              prop="currentStock"
              align="center"
          />
          <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
             label="入库时间"
             prop="createTime"
             width="200"
             show-overflow-tooltip
             v-if="searchForm.reportType !== 'inout'"
           />
           <el-table-column
             label="入库批次"
             prop="inboundBatches"
             width="240"
             show-overflow-tooltip
             v-if="searchForm.reportType !== 'inout'"
           />
           <el-table-column
             label="产品大类"
             prop="productName"
             show-overflow-tooltip
           />
           <el-table-column
             label="规格型号"
             prop="model"
             show-overflow-tooltip
           />
           <el-table-column
             label="单位"
             prop="unit"
             show-overflow-tooltip
           />
           <el-table-column
             label="入库数量"
             prop="totalStockIn"
             align="center"
             v-if="searchForm.reportType === 'inout'"
           />
           <el-table-column
               label="入库数量"
               prop="stockInNum"
               align="center"
               v-else
           />
           <el-table-column
             label="出库数量"
             prop="totalStockOut"
             width="100"
             align="center"
             v-if="searchForm.reportType === 'inout'"
           />
           <el-table-column
             label="现在库存"
             prop="currentStock"
             align="center"
           />
           <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>
      </el-card>
    </div>
@@ -307,7 +307,7 @@
  if (!validateSearchForm()) {
    return
  }
  tableLoading.value = true
  try {
    const params = getQueryParams()
@@ -325,7 +325,7 @@
      // nextTick(() => {
      //   initCharts()
      // })
    }
  } catch (error) {
    ElMessage.error('查询失败:' + error.message)
@@ -395,7 +395,7 @@
    startDate: "",
    endDate: ""
  }
  if (searchForm.reportType === 'daily') {
    params.reportDate = searchForm.singleDate
  } else if (searchForm.reportType === 'monthly') {
@@ -405,7 +405,7 @@
    params.startDate = searchForm.dateRange[0]
    params.endDate = searchForm.dateRange[1]
  }
  return params
}
@@ -427,7 +427,7 @@
  if (!validateSearchForm()) {
    return
  }
  try {
    const params = getQueryParams()
    // const response = await exportStockReport(params)
@@ -442,7 +442,7 @@
    // link.click()
    // document.body.removeChild(link)
    // window.URL.revokeObjectURL(url)
    // ElMessage.success('导出成功')
  } catch (error) {
    ElMessage.error('导出失败:' + error.message)
@@ -452,7 +452,7 @@
// åˆå§‹åŒ–图表
const initCharts = () => {
  if (!reportData.value.chartData) return
  initTrendChart()
  initComparisonChart()
}
@@ -460,7 +460,7 @@
// åˆå§‹åŒ–趋势图
const initTrendChart = () => {
  if (!trendChart.value) return
  const chart = echarts.init(trendChart.value)
  const option = {
    title: {
@@ -497,7 +497,7 @@
// åˆå§‹åŒ–对比图
const initComparisonChart = () => {
  if (!comparisonChart.value) return
  const chart = echarts.init(comparisonChart.value)
  const option = {
    title: {
@@ -544,7 +544,7 @@
onMounted(() => {
  const today = new Date()
  searchForm.singleDate = today.toISOString().split('T')[0]
  const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000)
  searchForm.dateRange = [
    yesterday.toISOString().split('T')[0],
src/views/procurementManagement/invoiceEntry/components/Modal.vue
@@ -58,6 +58,7 @@
                                                         v-model="form.invoiceAmount"
                                                         placeholder="请输入发票金额"
                                                         clearable
                             disabled
                        />
                    </el-form-item>
                </el-col>
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/procurementManagement/procurementReport/index.vue
@@ -73,12 +73,14 @@
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
import { ElMessage } from 'element-plus'
import { Download } from '@element-plus/icons-vue'
import PIMTable from '@/components/PIMTable/PIMTable.vue'
import { procurementBusinessSummaryListPage } from '@/api/procurementManagement/procurementReport'
import { productTreeList } from '@/api/basicData/product'
const { proxy } = getCurrentInstance()
// å“åº”式数据
const loading = ref(false)
@@ -269,7 +271,23 @@
}
const exportReport = () => {
  ElMessage.success('导出功能开发中...')
  const params = {}
  // æ—¶é—´èŒƒå›´
  if (searchForm.dateRange && searchForm.dateRange.length === 2) {
    params.entryDateStart = searchForm.dateRange[0]
    params.entryDateEnd = searchForm.dateRange[1]
  }
  // äº§å“ç±»åˆ«
  if (searchForm.productCategory) {
    const categoryName = findNodeLabelById(productOptions.value, searchForm.productCategory)
    if (categoryName) {
      params.productCategory = categoryName
    }
  }
  proxy.download("/procurementBusinessSummary/export", params, "采购业务汇总表.xlsx")
}
src/views/productionManagement/productStructure/Detail/index.vue
@@ -2,85 +2,136 @@
  <div class="app-container">
    <PageHeader content="产品结构详情">
      <template #right-button>
        <el-button v-if="!dataValue.isEdit && !isOrderPage" type="primary" @click="dataValue.isEdit = true">编辑
        <el-button v-if="!dataValue.isEdit && !isOrderPage"
                   type="primary"
                   @click="dataValue.isEdit = true">编辑
        </el-button>
        <el-button v-if="dataValue.isEdit && !isOrderPage" type="primary" @click="cancelEdit">取消
        <el-button v-if="dataValue.isEdit && !isOrderPage"
                   type="primary"
                   @click="cancelEdit">取消
        </el-button>
        <el-button v-if="!isOrderPage" type="primary" :loading="dataValue.loading" @click="submit"
          :disabled="!dataValue.isEdit">确认
        <el-button v-if="!isOrderPage"
                   type="primary"
                   :loading="dataValue.loading"
                   @click="submit"
                   :disabled="!dataValue.isEdit">确认
        </el-button>
      </template>
    </PageHeader>
    <el-table :data="tableData" border :preserve-expanded-content="false" :default-expand-all="true"
      style="width: 100%">
    <el-table :data="tableData"
              border
              :preserve-expanded-content="false"
              :default-expand-all="true"
              style="width: 100%">
      <el-table-column type="expand">
        <template #default="props">
          <el-form ref="form" :model="dataValue">
            <el-table :data="dataValue.dataList" row-key="tempId" default-expand-all
              :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" style="width: 100%">
              <el-table-column prop="productName" label="产品" />
              <el-table-column prop="model" label="规格">
          <el-form ref="form"
                   :model="dataValue">
            <el-table :data="dataValue.dataList"
                      row-key="tempId"
                      default-expand-all
                      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
                      style="width: 100%">
              <el-table-column prop="productName"
                               label="产品" />
              <el-table-column prop="model"
                               label="规格">
                <template #default="{ row, $index }">
                  <el-form-item v-if="dataValue.isEdit"
                    :rules="[{ required: true, message: '请选择规格', trigger: ['blur', 'change'] }]" style="margin: 0">
                    <el-select v-model="row.model" placeholder="请选择规格" clearable
                      :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)"
                      style="width: 100%" @visible-change="(v) => { if (v) openDialog(row.tempId) }">
                      <el-option v-if="row.model" :label="row.model" :value="row.model" />
                                :rules="[{ required: true, message: '请选择规格', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-select v-model="row.model"
                               placeholder="请选择规格"
                               clearable
                               :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)"
                               style="width: 100%"
                               @visible-change="(v) => { if (v) openDialog(row.tempId) }">
                      <el-option v-if="row.model"
                                 :label="row.model"
                                 :value="row.model" />
                    </el-select>
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column prop="processName" label="消耗工序">
              <el-table-column prop="processName"
                               label="消耗工序">
                <template #default="{ row, $index }">
                  <el-form-item v-if="dataValue.isEdit"
                    :rules="dataValue.dataList.some(item => (item as any).tempId === row.tempId) ? [] : [{ required: true, message: '请选择消耗工序', trigger: 'change' }]"
                    style="margin: 0">
                    <el-select v-model="row.processId" placeholder="请选择" filterable clearable style="width: 100%"
                      :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)">
                      <el-option v-for="item in dataValue.processOptions" :key="item.id" :label="item.name"
                        :value="item.id" />
                                :rules="dataValue.dataList.some(item => (item as any).tempId === row.tempId) ? [] : [{ required: true, message: '请选择消耗工序', trigger: 'change' }]"
                                style="margin: 0">
                    <el-select v-model="row.processId"
                               placeholder="请选择"
                               filterable
                               clearable
                               style="width: 100%"
                               :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)">
                      <el-option v-for="item in dataValue.processOptions"
                                 :key="item.id"
                                 :label="item.name"
                                 :value="item.id" />
                    </el-select>
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column prop="unitQuantity" label="单位产出所需数量">
              <el-table-column prop="unitQuantity"
                               label="单位产出所需数量">
                <template #default="{ row, $index }">
                  <el-form-item v-if="dataValue.isEdit"
                    :rules="[{ required: true, message: '请输入单位产出所需数量', trigger: ['blur', 'change'] }]"
                    style="margin: 0">
                    <el-input-number v-model="row.unitQuantity" :min="0" :precision="2" :step="1"
                      controls-position="right" style="width: 100%"
                      :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
                                :rules="[{ required: true, message: '请输入单位产出所需数量', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-input-number v-model="row.unitQuantity"
                                     :min="0"
                                     :precision="2"
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column v-if="isOrderPage" prop="demandedQuantity" label="需求总量">
              <el-table-column v-if="isOrderPage"
                               prop="demandedQuantity"
                               label="需求总量">
                <template #default="{ row, $index }">
                  <el-form-item v-if="dataValue.isEdit"
                    :rules="[{ required: true, message: '请输入需求总量', trigger: ['blur', 'change'] }]" style="margin: 0">
                    <el-input-number v-model="row.demandedQuantity" :min="0" :precision="2" :step="1"
                      controls-position="right" style="width: 100%"
                      :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
                                :rules="[{ required: true, message: '请输入需求总量', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-input-number v-model="row.demandedQuantity"
                                     :min="0"
                                     :precision="2"
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column prop="unit" label="单位">
              <el-table-column prop="unit"
                               label="单位">
                <template #default="{ row, $index }">
                  <el-form-item v-if="dataValue.isEdit"
                    :rules="[{ required: true, message: '请输入单位', trigger: ['blur', 'change'] }]" style="margin: 0">
                    <el-input v-model="row.unit" placeholder="请输入单位" clearable
                      :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
                                :rules="[{ required: true, message: '请输入单位', trigger: ['blur','change'] }]"
                                style="margin: 0">
                    <el-input v-model="row.unit"
                              placeholder="请输入单位"
                              clearable
                               :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
                  </el-form-item>
                </template>
              </el-table-column>
              <el-table-column label="操作" fixed="right" width="200">
              <el-table-column label="操作"
                               fixed="right"
                               width="200">
                <template #default="{ row, $index }">
                  <el-button
                    v-if="dataValue.isEdit && !dataValue.dataList.some(item => (item as any).tempId === row.tempId)"
                    type="danger" text @click="removeItem(row.tempId)">删除
                  <el-button v-if="dataValue.isEdit && !dataValue.dataList.some(item => (item as any).tempId === row.tempId)"
                             type="danger"
                             text
                             @click="removeItem(row.tempId)">删除
                  </el-button>
                  <el-button v-if="dataValue.isEdit" type="primary" text @click="addItem2(row.tempId)">添加
                  <el-button v-if="dataValue.isEdit"
                             type="primary"
                             text
                             @click="addItem2(row.tempId)">添加
                  </el-button>
                </template>
              </el-table-column>
@@ -88,271 +139,175 @@
          </el-form>
        </template>
      </el-table-column>
      <el-table-column label="BOM编号" prop="bomNo" />
      <el-table-column label="产品名称" prop="productName" />
      <el-table-column label="规格型号" prop="model" />
      <el-table-column label="BOM编号"
                       prop="bomNo" />
      <el-table-column label="产品名称"
                       prop="productName" />
      <el-table-column label="规格型号"
                       prop="model" />
    </el-table>
    <product-select-dialog v-if="dataValue.showProductDialog" v-model:model-value="dataValue.showProductDialog"
      @confirm="handleProduct" />
    <product-select-dialog v-if="dataValue.showProductDialog"
                           v-model:model-value="dataValue.showProductDialog"
                           @confirm="handleProduct" />
  </div>
</template>
<script setup lang="ts">
import {
  computed,
  defineAsyncComponent,
  defineComponent,
  onMounted,
  reactive,
  ref,
} from "vue";
import { queryList, add } from "@/api/productionManagement/productStructure.js";
import { listProcessBom } from "@/api/productionManagement/productionOrder.js";
import { list } from "@/api/productionManagement/productionProcess";
import { ElMessage } from "element-plus";
import { useRoute, useRouter } from "vue-router";
  import {
    computed,
    defineAsyncComponent,
    defineComponent,
    onMounted,
    reactive,
    ref,
  } from "vue";
  import { queryList, add } from "@/api/productionManagement/productStructure.js";
  import { listProcessBom } from "@/api/productionManagement/productionOrder.js";
  import { list } from "@/api/productionManagement/productionProcess";
  import { ElMessage } from "element-plus";
  import { useRoute, useRouter } from "vue-router";
defineComponent({
  name: "StructureEdit",
});
  defineComponent({
    name: "StructureEdit",
  });
const ProductSelectDialog = defineAsyncComponent(
  () => import("@/views/basicData/product/ProductSelectDialog.vue")
);
const emit = defineEmits(["update:router"]);
const form = ref();
  const ProductSelectDialog = defineAsyncComponent(
    () => import("@/views/basicData/product/ProductSelectDialog.vue")
  );
  const emit = defineEmits(["update:router"]);
  const form = ref();
const route = useRoute();
const router = useRouter();
const routeId = computed({
  get() {
    return route.query.id;
  },
  const route = useRoute();
  const router = useRouter();
  const routeId = computed({
    get() {
      return route.query.id;
    },
  set(val) {
    emit("update:router", val);
  },
});
    set(val) {
      emit("update:router", val);
    },
  });
// ä»Žè·¯ç”±å‚数获取产品信息
const routeBomNo = computed(() => route.query.bomNo || "");
const routeProductName = computed(() => route.query.productName || "");
const routeProductModelName = computed(
  () => route.query.productModelName || ""
);
const routeOrderId = computed(() => route.query.orderId);
const pageType = computed(() => route.query.type);
const isOrderPage = computed(
  () => pageType.value === "order" && routeOrderId.value
);
  // ä»Žè·¯ç”±å‚数获取产品信息
  const routeBomNo = computed(() => route.query.bomNo || "");
  const routeProductName = computed(() => route.query.productName || "");
  const routeProductModelName = computed(
    () => route.query.productModelName || ""
  );
  const routeOrderId = computed(() => route.query.orderId);
  const pageType = computed(() => route.query.type);
  const isOrderPage = computed(
    () => pageType.value === "order" && routeOrderId.value
  );
const dataValue = reactive({
  dataList: [],
  productOptions: [],
  processOptions: [],
  showProductDialog: false,
  currentRowIndex: null,
  currentRowName: null,
  loading: false,
  isEdit: false,
});
  const dataValue = reactive({
    dataList: [],
    productOptions: [],
    processOptions: [],
    showProductDialog: false,
    currentRowIndex: null,
    currentRowName: null,
    loading: false,
    isEdit: false,
  });
const tableData = reactive([
  {
    productName: "",
    model: "",
    bomNo: "",
  },
]);
  const tableData = reactive([
    {
      productName: "",
      model: "",
      bomNo: "",
    },
  ]);
const openDialog = (tempId: any) => {
  console.log(tempId, "tempId");
  dataValue.currentRowName = tempId;
  dataValue.showProductDialog = true;
};
  const openDialog = (tempId: any) => {
    console.log(tempId, "tempId");
    dataValue.currentRowName = tempId;
    dataValue.showProductDialog = true;
  };
const fetchData = async () => {
  if (isOrderPage.value) {
    // è®¢å•情况:使用订单的产品结构接口
    const { data } = await listProcessBom({ orderId: routeOrderId.value });
    dataValue.dataList = (data as any) || [];
  } else {
    // éžè®¢å•情况:使用原来的接口
    const { data } = await queryList(routeId.value);
    dataValue.dataList = (data as any) || [];
    // ä¸ºæ‰€æœ‰é¡¹åŠå…¶å­é¡¹è®¾ç½®name属性
    const setNameRecursively = (items: any[]) => {
      items.forEach((item: any) => {
        item.tempId = item.id;
        item.processName =
          dataValue.processOptions.find(option => option.id === item.processId)
            ?.name || "";
        if (item.children && item.children.length > 0) {
          setNameRecursively(item.children);
  const fetchData = async () => {
    if (isOrderPage.value) {
      // è®¢å•情况:使用订单的产品结构接口
      const { data } = await listProcessBom({ orderId: routeOrderId.value });
      dataValue.dataList = (data as any) || [];
    } else {
      // éžè®¢å•情况:使用原来的接口
      const { data } = await queryList(routeId.value);
      dataValue.dataList = (data as any) || [];
      // ä¸ºæ‰€æœ‰é¡¹åŠå…¶å­é¡¹è®¾ç½®name属性
      const setNameRecursively = (items: any[]) => {
        items.forEach((item: any) => {
          item.tempId = item.id;
          item.processName =
            dataValue.processOptions.find(option => option.id === item.processId)
              ?.name || "";
          if (item.children && item.children.length > 0) {
            setNameRecursively(item.children);
          }
        });
      };
      setNameRecursively(dataValue.dataList);
      console.log(dataValue.dataList, "dataValue.dataList");
    }
  };
  const fetchProcessOptions = async () => {
    const { data } = await list();
    dataValue.processOptions = data as any;
  };
  const handleProduct = (row: any) => {
    if (row?.length > 1) {
      ElMessage.error("只能选择一个产品");
    }
    const productData = row[0];
    //  æœ€å¤–层组件中,与当前产品相同的产品只能有一个
    const isTopLevel = dataValue.dataList.some(item => (item as any).tempId === dataValue.currentRowName);
    if (isTopLevel) {
      if (productData.productName === tableData[0].productName &&
        productData.model === tableData[0].model) {
        //  æŸ¥æ‰¾æ˜¯å¦å·²ç»æœ‰å…¶ä»–顶层行已经是这个产品
        const hasOther = dataValue.dataList.some(item =>
          (item as any).tempId !== dataValue.currentRowName &&
          (item as any).productName === tableData[0].productName &&
          (item as any).model === tableData[0].model
        );
        if (hasOther) {
          ElMessage.warning("最外层和当前产品一样的一级只能有一个");
          return;
        }
      });
    };
    setNameRecursively(dataValue.dataList);
    console.log(dataValue.dataList, "dataValue.dataList");
  }
};
const fetchProcessOptions = async () => {
  const { data } = await list();
  dataValue.processOptions = data as any;
};
const handleProduct = (row: any) => {
  if (row?.length > 1) {
    ElMessage.error("只能选择一个产品");
  }
  const productData = row[0];
  //  æœ€å¤–层组件中,与当前产品相同的产品只能有一个
  const isTopLevel = dataValue.dataList.some(item => (item as any).tempId === dataValue.currentRowName);
  if (isTopLevel) {
    if (productData.productName === tableData[0].productName &&
      productData.model === tableData[0].model) {
      //  æŸ¥æ‰¾æ˜¯å¦å·²ç»æœ‰å…¶ä»–顶层行已经是这个产品
      const hasOther = dataValue.dataList.some(item =>
        (item as any).tempId !== dataValue.currentRowName &&
        (item as any).productName === tableData[0].productName &&
        (item as any).model === tableData[0].model
      );
      if (hasOther) {
        ElMessage.warning("最外层和当前产品一样的一级只能有一个");
        return;
      }
    }
  }
  // dataValue.dataList[dataValue.currentRowIndex].productName =
  //   row[0].productName;
  // dataValue.dataList[dataValue.currentRowIndex].model = row[0].model;
  // dataValue.dataList[dataValue.currentRowIndex].productModelId = row[0].id;
  // dataValue.dataList[dataValue.currentRowIndex].unit = row[0].unit || "";
  dataValue.dataList.map(item => {
    if (item.tempId === dataValue.currentRowName) {
    // dataValue.dataList[dataValue.currentRowIndex].productName =
    //   row[0].productName;
    // dataValue.dataList[dataValue.currentRowIndex].model = row[0].model;
    // dataValue.dataList[dataValue.currentRowIndex].productModelId = row[0].id;
    // dataValue.dataList[dataValue.currentRowIndex].unit = row[0].unit || "";
    dataValue.dataList.map(item => {
      if (item.tempId === dataValue.currentRowName) {
        item.productName = productData.productName;
        item.model = productData.model;
        item.productModelId = productData.id;
        item.unit = productData.unit || "";
        return;
      }
      childItem(item, dataValue.currentRowName, productData);
    });
    dataValue.showProductDialog = false;
  };
  const childItem = (item: any, tempId: any, productData: any) => {
    if (item.tempId === tempId) {
      item.productName = productData.productName;
      item.model = productData.model;
      item.productModelId = productData.id;
      item.unit = productData.unit || "";
      return;
      return true;
    }
    childItem(item, dataValue.currentRowName, productData);
  });
  dataValue.showProductDialog = false;
};
const childItem = (item: any, tempId: any, productData: any) => {
  if (item.tempId === tempId) {
    item.productName = productData.productName;
    item.model = productData.model;
    item.productModelId = productData.id;
    item.unit = productData.unit || "";
    return true;
  }
  if (item.children && item.children.length > 0) {
    for (let child of item.children) {
      if (childItem(child, tempId, productData)) {
        return true;
      }
    }
  }
  return false;
};
// é€’归校验所有层级的表单数据
const validateAll = () => {
  let isValid = true;
  // æ ¡éªŒå‡½æ•°
  const validateItem = (item: any, isTopLevel = false) => {
    // æ ¡éªŒå½“前项的必填字段
    if (!item.model) {
      ElMessage.error("请选择规格");
      isValid = false;
      return;
    }
    if (!isTopLevel && !item.processId) {
      ElMessage.error("请选择消耗工序");
      isValid = false;
      return;
    }
    if (!item.unitQuantity) {
      ElMessage.error("请输入单位产出所需数量");
      isValid = false;
      return;
    }
    if (isOrderPage.value && !item.demandedQuantity) {
      ElMessage.error("请输入需求总量");
      isValid = false;
      return;
    }
    if (!item.unit) {
      ElMessage.error("请输入单位");
      isValid = false;
      return;
    }
    // é€’归校验子项
    if (item.children && item.children.length > 0) {
      item.children.forEach(child => {
        validateItem(child, false);
      });
    }
  };
  // éåŽ†æ‰€æœ‰é¡¶å±‚é¡¹
  dataValue.dataList.forEach(item => {
    validateItem(item, true);
  });
  return isValid;
};
const submit = () => {
  dataValue.loading = true;
  // å…ˆè¿›è¡Œè¡¨å•校验
  const valid = validateAll();
  console.log(dataValue.dataList, "dataValue.dataList");
  if (valid) {
    add({
      bomId: routeId.value,
      children: dataValue.dataList || [],
    })
      .then(res => {
        router.push({
          path: "/productionManagement/productionManagement/productStructure/index",
        });
        ElMessage.success("保存成功");
        dataValue.loading = false;
      })
      .catch(() => {
        dataValue.loading = false;
      });
  } else {
    dataValue.loading = false;
  }
};
const removeItem = (tempId: string) => {
  // å…ˆå°è¯•从顶层删除
  const topIndex = dataValue.dataList.findIndex(item => item.tempId === tempId);
  if (topIndex !== -1) {
    dataValue.dataList.splice(topIndex, 1);
    return;
  }
  // é€’归删除子项
  const delchildItem = (items: any[], tempId: any) => {
    for (let i = 0; i < items.length; i++) {
      const item = items[i];
      if (item.tempId === tempId) {
        items.splice(i, 1);
        return true;
      }
      if (item.children && item.children.length > 0) {
        if (delchildItem(item.children, tempId)) {
      for (let child of item.children) {
        if (childItem(child, tempId, productData)) {
          return true;
        }
      }
@@ -360,15 +315,142 @@
    return false;
  };
  dataValue.dataList.forEach(item => {
    if (item.children && item.children.length > 0) {
      delchildItem(item.children, tempId);
  // é€’归校验所有层级的表单数据
  const validateAll = () => {
    let isValid = true;
    // æ ¡éªŒå‡½æ•°
    const validateItem = (item: any, isTopLevel = false) => {
      // æ ¡éªŒå½“前项的必填字段
      if (!item.model) {
        ElMessage.error("请选择规格");
        isValid = false;
        return;
      }
      if (!isTopLevel && !item.processId) {
        ElMessage.error("请选择消耗工序");
        isValid = false;
        return;
      }
      if (!item.unitQuantity) {
        ElMessage.error("请输入单位产出所需数量");
        isValid = false;
        return;
      }
      if (isOrderPage.value && !item.demandedQuantity) {
        ElMessage.error("请输入需求总量");
        isValid = false;
        return;
      }
      // if (!item.unit) {
      //   ElMessage.error("请输入单位");
      //   isValid = false;
      //   return;
      // }
      // é€’归校验子项
      if (item.children && item.children.length > 0) {
        item.children.forEach(child => {
          validateItem(child, false);
        });
      }
    };
    // éåŽ†æ‰€æœ‰é¡¶å±‚é¡¹
    dataValue.dataList.forEach(item => {
      validateItem(item, true);
    });
    return isValid;
  };
  const submit = () => {
    dataValue.loading = true;
    // å…ˆè¿›è¡Œè¡¨å•校验
    const valid = validateAll();
    console.log(dataValue.dataList, "dataValue.dataList");
    if (valid) {
      add({
        bomId: routeId.value,
        children: dataValue.dataList || [],
      })
        .then(res => {
          router.push({
            path: "/productionManagement/productionManagement/productStructure/index",
          });
          ElMessage.success("保存成功");
          dataValue.loading = false;
        })
        .catch(() => {
          dataValue.loading = false;
        });
    } else {
      dataValue.loading = false;
    }
  });
};
const addItem2 = tempId => {
  dataValue.dataList.map(item => {
  };
  const removeItem = (tempId:string) => {
    // å…ˆå°è¯•从顶层删除
    const topIndex = dataValue.dataList.findIndex(item => item.tempId === tempId);
    if (topIndex !== -1) {
      dataValue.dataList.splice(topIndex, 1);
      return;
    }
    // é€’归删除子项
    const delchildItem = (items: any[], tempId: any) => {
      for (let i = 0; i < items.length; i++) {
        const item = items[i];
        if (item.tempId === tempId) {
          items.splice(i, 1);
          return true;
        }
        if (item.children && item.children.length > 0) {
          if (delchildItem(item.children, tempId)) {
            return true;
          }
        }
      }
      return false;
    };
    dataValue.dataList.forEach(item => {
      if (item.children && item.children.length > 0) {
        delchildItem(item.children, tempId);
      }
    });
  };
  const addItem2 = tempId => {
    dataValue.dataList.map(item => {
      if (item.tempId === tempId) {
        if (!item.children) {
          item.children = [];
        }
        item.children.push({
          parentId: item.id || "",
          parentTempId: item.tempId || "",
          productName: "",
          productId: "",
          model: undefined,
          productModelId: undefined,
          processId: "",
          processName: "",
          unitQuantity: 0,
          demandedQuantity: 0,
          unit: "",
          children: [],
          tempId: new Date().getTime(),
        });
        return;
      }
      addchildItem(item, tempId);
    });
  };
  const addchildItem = (item: any, tempId: any) => {
    if (item.tempId === tempId) {
      console.log(item, "item");
      if (!item.children) {
        item.children = [];
      }
@@ -380,70 +462,55 @@
        model: undefined,
        productModelId: undefined,
        processId: "",
        processName: "",
        unitQuantity: 0,
        demandedQuantity: 0,
        unit: "",
        children: [],
        unit: "",
        tempId: new Date().getTime(),
      });
      return;
      return true;
    }
    addchildItem(item, tempId);
  });
};
const addchildItem = (item: any, tempId: any) => {
  if (item.tempId === tempId) {
    console.log(item, "item");
    if (!item.children) {
      item.children = [];
    }
    item.children.push({
      parentId: item.id || "",
      parentTempId: item.tempId || "",
      productName: "",
      productId: "",
      model: undefined,
      productModelId: undefined,
      processId: "",
      unitQuantity: 0,
      demandedQuantity: 0,
      children: [],
      unit: "",
      tempId: new Date().getTime(),
    });
    return true;
  }
  if (item.children && item.children.length > 0) {
    for (let child of item.children) {
      if (addchildItem(child, tempId)) {
        return true;
    if (item.children && item.children.length > 0) {
      for (let child of item.children) {
        if (addchildItem(child, tempId)) {
          return true;
        }
      }
    }
  }
  return false;
};
    return false;
  };
const cancelEdit = () => {
  dataValue.isEdit = false;
  // dataValue.dataList = dataValue.dataList.filter(item => item.id !== undefined);
  fetchData();
};
  const getPropPath = (row, field) => {
    // ä¸ºæ¯ä¸ªrow生成唯一的路径
    // ä½¿ç”¨row.id或索引作为唯一标识
    let path = "dataList";
onMounted(async () => {
  // ä»Žè·¯ç”±å‚数回显数据
  tableData[0].productName = routeProductName.value as string;
  tableData[0].model = routeProductModelName.value as string;
  tableData[0].bomNo = routeBomNo.value as string;
    // ç®€å•实现:使用row的id或一个唯一标识
    const uniqueId = row.id || Math.floor(Math.random() * 10000);
    path += `.${uniqueId}`;
  // è®¢å•情况下禁用编辑
  if (isOrderPage.value) {
    return path + `.${field}`;
  };
  const cancelEdit = () => {
    dataValue.isEdit = false;
  }
    // dataValue.dataList = dataValue.dataList.filter(item => item.id !== undefined);
    fetchData();
  };
  // å…ˆåŠ è½½å·¥åºé€‰é¡¹ï¼Œå†åŠ è½½æ•°æ®ï¼Œç¡®ä¿el-select能够正确回显
  await fetchProcessOptions();
  await fetchData();
});
  onMounted(async () => {
    // ä»Žè·¯ç”±å‚数回显数据
    tableData[0].productName = routeProductName.value as string;
    tableData[0].model = routeProductModelName.value as string;
    tableData[0].bomNo = routeBomNo.value as string;
    // è®¢å•情况下禁用编辑
    if (isOrderPage.value) {
      dataValue.isEdit = false;
    }
    // å…ˆåŠ è½½å·¥åºé€‰é¡¹ï¼Œå†åŠ è½½æ•°æ®ï¼Œç¡®ä¿el-select能够正确回显
    await fetchProcessOptions();
    await fetchData();
  });
</script>
src/views/productionManagement/productionDispatching/components/formDia.vue
@@ -111,6 +111,7 @@
<script setup>
import {ref} from "vue";
import {getStaffJoinInfo, staffJoinAdd, staffJoinUpdate} from "@/api/personnelManagement/onboarding.js";
import {userListNoPageByTenantId} from "@/api/system/user.js";
import {productionDispatch} from "@/api/productionManagement/productionOrder.js";
import useUserStore from "@/store/modules/user.js";
src/views/productionManagement/productionReporting/components/formDia.vue
@@ -94,6 +94,7 @@
<script setup>
import {ref} from "vue";
import {getStaffJoinInfo, staffJoinAdd, staffJoinUpdate} from "@/api/personnelManagement/onboarding.js";
import {userListNoPageByTenantId} from "@/api/system/user.js";
import {productionReport, productionReportUpdate} from "@/api/productionManagement/productionReporting.js";
const { proxy } = getCurrentInstance()
src/views/productionManagement/workOrder/components/filesDia.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,202 @@
<template>
  <div>
    <el-dialog v-model="dialogVisible" title="工单附件" width="50%" @close="closeDia">
      <div style="margin-bottom: 10px; text-align: right">
        <el-upload
          v-model:file-list="fileList"
          class="upload-demo"
          :action="uploadUrl"
          :on-success="handleUploadSuccess"
          :on-error="handleUploadError"
          :before-upload="beforeUpload"
          name="file"
          :show-file-list="false"
          :headers="headers"
          accept="image/*"
          style="display: inline; margin-right: 10px"
        >
          <el-button type="primary">上传图片</el-button>
        </el-upload>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :total="page.total"
        :tableLoading="tableLoading"
        :isSelection="true"
        @selection-change="handleSelectionChange"
        @pagination="paginationSearch"
        height="500"
      />
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="closeDia">关闭</el-button>
        </div>
      </template>
    </el-dialog>
    <filePreview ref="filePreviewRef" />
  </div>
</template>
<script setup>
import { ref, reactive, getCurrentInstance } from "vue";
import { ElMessageBox } from "element-plus";
import { getToken } from "@/utils/auth.js";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import filePreview from "@/components/filePreview/index.vue";
import {
  productWorkOrderFileAdd,
  productWorkOrderFileDel,
  productWorkOrderFileListPage,
} from "@/api/productionManagement/productWorkOrderFile.js";
const { proxy } = getCurrentInstance();
const emit = defineEmits(["close"]);
const dialogVisible = ref(false);
const currentWorkOrderId = ref("");
const selectedRows = ref([]);
const filePreviewRef = ref();
const tableColumn = ref([
  {
    label: "文件名称",
    prop: "name",
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    width: 120,
    operation: [
      {
        name: "下载",
        type: "text",
        clickFun: row => {
          proxy.$download.name(row.url);
        },
      },
      {
        name: "预览",
        type: "text",
        clickFun: row => {
          filePreviewRef.value?.open(row.url);
        },
      },
    ],
  },
]);
const page = reactive({
  current: 1,
  size: 100,
  total: 0,
});
const tableData = ref([]);
const fileList = ref([]);
const tableLoading = ref(false);
const headers = ref({
  Authorization: "Bearer " + getToken(),
});
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload");
const beforeUpload = file => {
  const isImage = file?.type?.startsWith("image/");
  if (!isImage) {
    proxy.$modal.msgError("只能上传图片文件");
  }
  return isImage;
};
const openDialog = row => {
  dialogVisible.value = true;
  currentWorkOrderId.value = row.id;
  page.current = 1;
  getList();
};
const closeDia = () => {
  dialogVisible.value = false;
  emit("close");
};
const paginationSearch = obj => {
  page.current = obj.page;
  page.size = obj.limit;
  getList();
};
const getList = () => {
  tableLoading.value = true;
  productWorkOrderFileListPage({
    workOrderId: currentWorkOrderId.value,
    current: page.current,
    size: page.size,
  })
    .then(res => {
      tableData.value = res.data.records || [];
      page.total = res.data.total || 0;
    })
    .finally(() => {
      tableLoading.value = false;
    });
};
const handleSelectionChange = selection => {
  selectedRows.value = selection;
};
function handleUploadSuccess(res) {
  if (res.code == 200) {
    const fileRow = {
      name: res.data.originalName,
      url: res.data.tempPath,
      workOrderId: currentWorkOrderId.value,
    };
    productWorkOrderFileAdd(fileRow).then(() => {
      proxy.$modal.msgSuccess("文件上传成功");
      getList();
    });
  } else {
    proxy.$modal.msgError("文件上传失败");
  }
}
function handleUploadError() {
  proxy.$modal.msgError("文件上传失败");
}
const handleDelete = () => {
  if (selectedRows.value.length === 0) {
    proxy.$modal.msgWarning("请选择数据");
    return;
  }
  const ids = selectedRows.value.map(item => item.id);
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      productWorkOrderFileDel(ids).then(() => {
        proxy.$modal.msgSuccess("删除成功");
        getList();
      });
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
};
defineExpose({
  openDialog,
});
</script>
<style scoped></style>
src/views/productionManagement/workOrder/index.vue
@@ -209,6 +209,7 @@
        </span>
      </template>
    </el-dialog>
    <FilesDia ref="workOrderFilesRef" />
  </div>
</template>
@@ -220,10 +221,12 @@
    productWorkOrderPage,
    updateProductWorkOrder,
    addProductMain,
    downProductWorkOrder,
  } from "@/api/productionManagement/workOrder.js";
  import { getUserProfile, userListNoPageByTenantId } from "@/api/system/user.js";
  import QRCode from "qrcode";
  import { getCurrentInstance, reactive, toRefs } from "vue";
  import FilesDia from "./components/filesDia.vue";
  const { proxy } = getCurrentInstance();
  const tableColumn = ref([
@@ -307,7 +310,13 @@
        {
          name: "流转卡",
          clickFun: row => {
            showTransferCard(row);
            downloadAndPrintWorkOrder(row);
          },
        },
        {
          name: "附件",
          clickFun: row => {
            openWorkOrderFiles(row);
          },
        },
        {
@@ -330,6 +339,7 @@
  const transferCardQrUrl = ref("");
  const transferCardRowData = ref(null);
  const reportDialogVisible = ref(false);
  const workOrderFilesRef = ref(null);
  const userOptions = ref([]);
  const reportForm = reactive({
    planQuantity: 0,
@@ -395,6 +405,56 @@
      });
  };
  // ä¸‹è½½å¹¶æ‰“印工单流转卡(文件流)
  const downloadAndPrintWorkOrder = async row => {
    if (!row || !row.id) {
      proxy.$modal.msgError("缺少工单ID,无法下载流转卡");
      return;
    }
    const fileName = row.workOrderNo
      ? `工单流转卡_${row.workOrderNo}.xlsx`
      : "工单流转卡.xlsx";
    try {
      // è°ƒç”¨æŽ¥å£ï¼Œä»¥ responseType: 'blob' èŽ·å–æ–‡ä»¶æµ
      const blob = await downProductWorkOrder(row.id);
      if (!blob) {
        proxy.$modal.msgError("未获取到流转卡文件");
        return;
      }
      // åˆ›å»º Blob URL
      const fileBlob =
        blob instanceof Blob ? blob : new Blob([blob], { type: blob.type || "application/octet-stream" });
      const url = window.URL.createObjectURL(fileBlob);
      // åˆ›å»ºéšè— iframe,用于触发浏览器打印
      const iframe = document.createElement("iframe");
      iframe.style.position = "fixed";
      iframe.style.right = "0";
      iframe.style.bottom = "0";
      iframe.style.width = "0";
      iframe.style.height = "0";
      iframe.style.border = "0";
      iframe.src = url;
      document.body.appendChild(iframe);
      iframe.onload = () => {
        try {
          iframe.contentWindow?.focus();
          iframe.contentWindow?.print();
        } catch (e) {
          console.error("自动调用打印失败", e);
          // é€€è€Œæ±‚其次,打开新窗口由用户手动打印
          window.open(url);
        }
      };
    } catch (e) {
      console.error("下载工单流转卡失败", e);
      proxy.$modal.msgError("下载工单流转卡失败");
    }
  };
  const showTransferCard = async row => {
    transferCardRowData.value = row;
    const qrContent = String(row.id);
@@ -450,6 +510,10 @@
    reportDialogVisible.value = true;
  };
  const openWorkOrderFiles = row => {
    workOrderFilesRef.value?.openDialog(row);
  };
  const handleReport = () => {
    if (reportForm.planQuantity <= 0) {
      ElMessageBox.alert("待生产数量为0,无法报工", "提示", {
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue
@@ -34,6 +34,7 @@
                  @change="getModels"
                  :data="productOptions"
                  :render-after-expand="false"
                  :disabled="operationType === 'edit'"
                  style="width: 100%"
              />
            </el-form-item>
@@ -41,8 +42,11 @@
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="规格型号:" prop="model">
              <el-input v-model="form.model" placeholder="请输入" clearable/>
            <el-form-item label="规格型号:" prop="productModelId">
              <el-select v-model="form.productModelId" placeholder="请选择" clearable :disabled="operationType === 'edit'"
                         filterable readonly @change="handleChangeModel">
                <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
@@ -67,7 +71,7 @@
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="单位:" prop="unit">
              <el-input v-model="form.unit" placeholder="请输入" clearable/>
              <el-input v-model="form.unit" disabled/>
            </el-form-item>
          </el-col>
          <el-col :span="12">
@@ -141,9 +145,8 @@
<script setup>
import {ref, reactive, toRefs, getCurrentInstance, nextTick} from "vue";
import {getOptions} from "@/api/procurementManagement/procurementLedger.js";
import {productTreeList} from "@/api/basicData/product.js";
import {modelList, productTreeList} from "@/api/basicData/product.js";
import {qualityInspectAdd, qualityInspectUpdate} from "@/api/qualityManagement/rawMaterialInspection.js";
import {ElMessageBox} from "element-plus";
import {qualityInspectParamDel, qualityInspectParamInfo} from "@/api/qualityManagement/qualityInspectParam.js";
import {qualityInspectDetailByProductId, getQualityTestStandardParamByTestStandardId} from "@/api/qualityManagement/metricMaintenance.js";
@@ -159,6 +162,7 @@
    checkName: "",
    productName: "",
    productId: "",
    productModelId: "",
    model: "",
    testStandardId: "",
    unit: "",
@@ -171,7 +175,7 @@
    supplier: [{required: true, message: "请输入", trigger: "blur"}],
    checkName: [{required: false, message: "请输入", trigger: "blur"}],
    productId: [{required: true, message: "请输入", trigger: "blur"}],
    model: [{required: false, message: "请输入", trigger: "blur"}],
    productModelId: [{required: false, message: "请选择产品型号", trigger: "change"}],
    testStandardId: [{required: false, message: "请选择指标", trigger: "change"}],
    unit: [{required: false, message: "请输入", trigger: "blur"}],
    quantity: [{required: true, message: "请输入", trigger: "blur"}],
@@ -211,6 +215,7 @@
const productOptions = ref([]);
const currentProductId = ref(0);
const testStandardOptions = ref([]); // æŒ‡æ ‡é€‰æ‹©ä¸‹æ‹‰æ¡†æ•°æ®
const modelOptions = ref([]);
// æ‰“开弹框
const openDialog = (type, row) => {
@@ -275,12 +280,23 @@
  });
};
const getModels = (value) => {
  form.value.productModelId = undefined;
  modelOptions.value = [];
  currentProductId.value = value
  form.value.productName = findNodeById(productOptions.value, value);
  modelList({ id: value }).then((res) => {
    modelOptions.value = res;
  })
  if (currentProductId.value) {
    getList();
  }
};
const handleChangeModel = (value) => {
  form.value.model = modelOptions.value.find(item => item.id == value)?.model || '';
  form.value.unit = modelOptions.value.find(item => item.id == value)?.unit || '';
}
const findNodeById = (nodes, productId) => {
  for (let i = 0; i < nodes.length; i++) {
    if (nodes[i].value === productId) {
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>