天津_意博凯信
1.备件管理-新增独立备件出库功能入口,支持直接完成备件领用出库操作。
2.设备保养-支持按日期分组折叠展示,分组条目可展开查看明细;单条明细行末尾增加【保养完成】操作按钮,可单独办结单条保养任务;折叠汇总状态下,新增【一键保养完成】批量操作按钮,快速办结当前分组全部保养项。
3.设备巡检-按日期分组折叠展示,分组支持展开查看明细;每条巡检明细后增设【巡检完成】按钮,支持单独办结单条巡检记录;分组折叠未展开状态下,提供【一键巡检完成】批量操作,统一办结该分组下所有巡检任务。
4.增值税对比-优化数据展示逻辑,列表展示各订单增值税明细;页面右侧新增柱状可视化图表,直观对比进销项增值税数据。
5.管理驾驶舱-指标修改:将顶部「销售产品数」调整为「销售订单数」;时间筛选:页面左上角新增日期范围下拉筛选控件,所有统计指标标注当前筛选时段;
已修改17个文件
6324 ■■■■ 文件已修改
src/api/equipmentManagement/sparePartsUsage.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/upkeep.js 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inspectionManagement/index.js 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/procurementManagement/taxComparison.js 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/viewIndex.js 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/index.vue 741 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/spareParts/index.vue 931 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/index.vue 1282 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/center-bottom.vue 263 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/center-center.vue 190 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/center-top.vue 161 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue 356 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue 306 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/index.vue 458 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue 510 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue 550 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/taxComparison/index.vue 491 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/sparePartsUsage.js
@@ -34,3 +34,12 @@
  });
};
// 备件直接出库
export function outboundSpareParts(data) {
  return request({
    url: '/sparePartsRequisitionRecord/outbound',
    method: 'post',
    data
  })
}
src/api/equipmentManagement/upkeep.js
@@ -94,6 +94,23 @@
    params: params,
  });
};
// 单条保养完成
export function completeMaintenance(id) {
  return request({
    url: '/device/maintenance/complete/' + id,
    method: 'put'
  })
}
// 一键保养完成
export function batchCompleteMaintenance(dateStr) {
  return request({
    url: '/device/maintenance/batchComplete',
    method: 'put',
    params: { dateStr }
  })
}
// 设备保养定时任务列表
export const deviceMaintenanceTaskDel = (params) => {
  return request({
src/api/inspectionManagement/index.js
@@ -58,4 +58,21 @@
        method: 'post',
        data: query
    })
}
// 单条巡检完成
export function completeInspection(id) {
    return request({
        url: '/inspectionTask/complete/' + id,
        method: 'put'
    })
}
// 一键巡检完成
export function batchCompleteInspection(dateStr) {
    return request({
        url: '/inspectionTask/batchComplete',
        method: 'put',
        params: { dateStr }
    })
}
src/api/procurementManagement/taxComparison.js
@@ -1,6 +1,6 @@
import request from "@/utils/request";
// 分页查询
// 分页查询(新增 type 参数)
export function getTaxList(query) {
  return request({
    url: "/purchase/report/listVat",
@@ -8,3 +8,21 @@
    params: query,
  });
}
// 增值税进销项图表数据
export function getVatChart(year) {
  return request({
    url: '/purchase/report/vatChart',
    method: 'get',
    params: { year }
  })
}
// 增值税明细
export function getVatDetail(query) {
  return request({
    url: '/purchase/report/listVatDetail',
    method: 'get',
    params: query
  })
}
src/api/viewIndex.js
@@ -202,20 +202,20 @@
};
// 供应商采购排名
export const supplierPurchaseRanking = (query) => {
export const supplierPurchaseRanking = (params) => {
  return request({
    url: "/home/supplierPurchaseRanking",
    method: "get",
    params: query,
    params,
  });
};
// 客户金额贡献排名
export const customerContributionRanking = (query) => {
export const customerContributionRanking = (params) => {
  return request({
    url: "/home/customerContributionRanking",
    method: "get",
    params: query,
    params,
  });
};
@@ -228,10 +228,11 @@
};
// 产品销售金额分析
export const productSalesAnalysis = () => {
export const productSalesAnalysis = (params) => {
  return request({
    url: "/home/productSalesAnalysis",
    method: "get",
    params,
  });
};
@@ -245,10 +246,11 @@
};
// 原材料采购金额占比
export const rawMaterialPurchaseAmountRatio = () => {
export const rawMaterialPurchaseAmountRatio = (params) => {
  return request({
    url: "/home/rawMaterialPurchaseAmountRatio",
    method: "get",
    params,
  });
};
@@ -262,10 +264,11 @@
};
// 销售/采购/储存产品数
export const salesPurchaseStorageProductCount = () => {
export const salesPurchaseStorageProductCount = (startDate, endDate) => {
  return request({
    url: "/home/salesPurchaseStorageProductCount",
    method: "get",
    params: { startDate, endDate },
  });
};
@@ -288,10 +291,11 @@
};
// 产品周转天数
export const productTurnoverDays = () => {
export const productTurnoverDays = (params) => {
  return request({
    url: "/home/productTurnoverDays",
    method: "get",
    params,
  });
};
@@ -349,7 +353,7 @@
export const productionOrderProgress = (params = {}) => {
  const safePageNum = Math.max(1, Number(params.pageNum || 1));
  const safeTab = ["all", "inProgress", "completed", "end"].includes(params.tab)
  const safeTab = ["all", "inProgress", "completed", "paused"].includes(params.tab)
    ? params.tab
    : "all";
  return request({
src/views/equipmentManagement/inspectionManagement/index.vue
@@ -1,449 +1,366 @@
<template>
  <div class="app-container">
    <el-form :inline="true"
             :model="queryParams"
             class="search-form">
      <el-form-item label="巡检任务名称">
        <el-input v-model="queryParams.taskName"
                  placeholder="请输入巡检任务名称"
                  clearable
                  style="width: 200px " />
      </el-form-item>
      <el-form-item>
        <el-button type="primary"
                   @click="handleQuery">查询</el-button>
        <el-button @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <el-card>
      <div style="display: flex;flex-direction: row;justify-content: space-between;margin-bottom: 10px;">
        <el-radio-group v-model="activeRadio"
                        @change="radioChange">
          <el-radio-button v-for="tab in radios"
                           :key="tab.name"
                           :label="tab.label"
                           :value="tab.name" />
        </el-radio-group>
        <!-- 操作按钮区 -->
        <el-space v-if="activeRadio !== 'task'">
          <el-button type="primary"
                     :icon="Plus"
                     @click="handleAdd(undefined)">新建</el-button>
          <el-button type="danger"
                     :icon="Delete"
                     @click="handleDelete">删除</el-button>
          <el-button @click="handleOut">导出</el-button>
        </el-space>
        <el-space v-else>
          <el-button @click="handleOut">导出</el-button>
        </el-space>
      </div>
      <div>
        <PIMTable :table-loading="tableLoading"
                  :table-data="tableData"
                  :column="tableColumns"
                  @selection-change="handleSelectionChange"
                  @pagination="handlePagination"
                  :is-selection="true"
                  :border="true"
                  :page="{
    <div class="app-container">
        <el-form :inline="true"
                 :model="queryParams"
                 class="search-form">
            <el-form-item label="巡检任务名称">
                <el-input v-model="queryParams.taskName"
                          placeholder="请输入巡检任务名称"
                          clearable
                          style="width: 200px " />
            </el-form-item>
            <el-form-item>
                <el-button type="primary"
                           @click="handleQuery">查询</el-button>
                <el-button @click="resetQuery">重置</el-button>
            </el-form-item>
        </el-form>
        <el-card>
            <div style="display: flex;flex-direction: row;justify-content: space-between;margin-bottom: 10px;">
                <el-radio-group v-model="activeRadio"
                                @change="radioChange">
                    <el-radio-button v-for="tab in radios"
                                     :key="tab.name"
                                     :label="tab.label"
                                     :value="tab.name" />
                </el-radio-group>
                <el-space v-if="activeRadio !== 'task'">
                    <el-button type="primary"
                               :icon="Plus"
                               @click="handleAdd(undefined)">新建</el-button>
                    <el-button type="danger"
                               :icon="Delete"
                               @click="handleDelete">删除</el-button>
                    <el-button @click="handleOut">导出</el-button>
                </el-space>
                <el-space v-else>
                    <el-button @click="handleOut">导出</el-button>
                </el-space>
            </div>
            <!-- 巡检任务 -->
            <div v-show="activeRadio === 'taskManage'">
                <PIMTable :table-loading="tableLoading"
                          :table-data="tableData"
                          :column="tableColumns"
                          @selection-change="handleSelectionChange"
                          @pagination="handlePagination"
                          :is-selection="true"
                          :border="true"
                          :page="{
                  current: pageNum,
                  size: pageSize,
                  total: total,
                  layout: 'total, sizes, prev, pager, next, jumper'
                }"
                  height="calc(100vh - 23em)"
                  :table-style="{ width: '100%' }">
          <template #inspector="{ row }">
            <div class="person-tags">
              <!-- 调试信息,上线时删除 -->
              <!-- {{ console.log('inspector data:', row.inspector) }} -->
              <template v-if="row.inspector && row.inspector.length > 0">
                <el-tag v-for="(person, index) in row.inspector"
                        :key="index"
                        size="small"
                        type="primary"
                        class="person-tag">
                  {{ person }}
                </el-tag>
              </template>
              <span v-else
                    class="no-data">--</span>
            </div>
          </template>
          <template #isEnabled="{ row }">
            <el-tag :type="row.isEnabled === 1 ? 'success' : 'danger'"
                    size="small">
              {{ row.isEnabled == 1 ? '是' : '否' }}
            </el-tag>
          </template>
        </PIMTable>
      </div>
    </el-card>
    <form-dia ref="formDia"
              @closeDia="handleQuery"></form-dia>
    <view-files ref="viewFiles"></view-files>
    <upload-files ref="uploadFiles"
                  @success="handleQuery"
                  @closeDia="handleQuery"></upload-files>
  </div>
                          height="calc(100vh - 23em)"
                          :table-style="{ width: '100%' }">
                    <template #inspector="{ row }">
                        <div class="person-tags">
                            <template v-if="row.inspector && row.inspector.length > 0">
                                <el-tag v-for="(person, index) in row.inspector"
                                        :key="index"
                                        size="small"
                                        type="primary"
                                        class="person-tag">
                                    {{ person }}
                                </el-tag>
                            </template>
                            <span v-else class="no-data">--</span>
                        </div>
                    </template>
                    <template #isEnabled="{ row }">
                        <el-tag :type="row.isEnabled === 1 ? 'success' : 'danger'"
                                size="small">
                            {{ row.isEnabled == 1 ? '是' : '否' }}
                        </el-tag>
                    </template>
                </PIMTable>
            </div>
            <!-- 巡检记录 - 日期分组折叠 -->
            <div v-show="activeRadio === 'task'">
                <el-table
                    :data="groupedInspectionList"
                    border
                    v-loading="tableLoading"
                    :expand-row-keys="inspectionExpandedRowKeys"
                    :row-key="(row) => row.date"
                    @expand-change="onInspectionExpandChange"
                    height="calc(100vh - 23em)"
                >
                    <el-table-column type="expand">
                        <template #default="props">
                            <el-table :data="props.row.children" border row-key="id">
                                <el-table-column label="巡检任务名称" prop="taskName" align="center" min-width="180" />
                                <el-table-column label="巡检项目" prop="inspectionProject" align="center" min-width="160" />
                                <el-table-column label="巡检人" align="center" min-width="150">
                                    <template #default="{ row }">
                                        <template v-if="row.inspector && row.inspector.length > 0">
                                            <el-tag v-for="(p, i) in row.inspector" :key="i" size="small" type="primary" class="person-tag">{{ p }}</el-tag>
                                        </template>
                                        <span v-else>--</span>
                                    </template>
                                </el-table-column>
                                <el-table-column label="巡检状态" align="center" width="100">
                                    <template #default="{ row }">
                                        <el-tag :type="row.status === 1 ? 'success' : 'warning'" size="small">
                                            {{ row.status === 1 ? '已完成' : '待巡检' }}
                                        </el-tag>
                                    </template>
                                </el-table-column>
                                <el-table-column label="巡检结果" prop="inspectionResult" align="center" min-width="120" />
                                <el-table-column label="备注" prop="remarks" align="center" min-width="150" />
                                <el-table-column label="异常描述" prop="abnormalDescription" align="center" min-width="150" />
                                <el-table-column label="操作" align="center" width="200">
                                    <template #default="{ row }">
                                        <el-button type="primary" link @click="openUploadDialog(row)">上传</el-button>
                                        <el-button type="success" link @click="viewFile(row)">查看附件</el-button>
                                        <el-button type="warning" link v-if="row.status === 0" @click="handleSingleComplete(row)">巡检完成</el-button>
                                    </template>
                                </el-table-column>
                            </el-table>
                        </template>
                    </el-table-column>
                    <el-table-column label="巡检日期" prop="date" align="center" />
                    <el-table-column label="巡检记录数" prop="count" align="center" />
                    <el-table-column label="待巡检数" align="center" width="120">
                        <template #default="{ row }">
                            {{ row.children.filter(c => c.status === 0).length }}
                        </template>
                    </el-table-column>
                    <el-table-column label="操作" align="center" width="200">
                        <template #default="{ row }">
                            <el-button
                                type="primary"
                                size="small"
                                :disabled="!row.children.some(c => c.status === 0)"
                                @click="handleBatchInspection(row.date)"
                            >
                                一键巡检完成
                            </el-button>
                        </template>
                    </el-table-column>
                </el-table>
                <Pagination
                    v-show="total > 0"
                    :total="total"
                    layout="total, sizes, prev, pager, next, jumper"
                    :page="pageNum"
                    :limit="pageSize"
                    @pagination="handlePagination"
                />
            </div>
        </el-card>
        <form-dia ref="formDia"
                  @closeDia="handleQuery"></form-dia>
        <view-files ref="viewFiles"></view-files>
        <upload-files ref="uploadFiles"
                      @success="handleQuery"
                      @closeDia="handleQuery"></upload-files>
    </div>
</template>
<script setup>
  import { Delete, Plus } from "@element-plus/icons-vue";
  import { onMounted, ref, reactive, getCurrentInstance, nextTick } from "vue";
  import { ElMessageBox } from "element-plus";
  import dayjs from "dayjs";
import { Delete, Plus } from "@element-plus/icons-vue";
import { onMounted, ref, reactive, getCurrentInstance, nextTick, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import dayjs from "dayjs";
  // 组件引入
  import PIMTable from "@/components/PIMTable/PIMTable.vue";
  import FormDia from "@/views/equipmentManagement/inspectionManagement/components/formDia.vue";
  import UploadFiles from "@/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue";
  import ViewFiles from "@/views/equipmentManagement/inspectionManagement/components/viewFiles.vue";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import FormDia from "@/views/equipmentManagement/inspectionManagement/components/formDia.vue";
import UploadFiles from "@/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue";
import ViewFiles from "@/views/equipmentManagement/inspectionManagement/components/viewFiles.vue";
  // 接口引入
  import {
    delTimingTask,
    inspectionTaskList,
    timingTaskList,
  } from "@/api/inspectionManagement/index.js";
import {
    delTimingTask,
    inspectionTaskList,
    timingTaskList,
    completeInspection,
    batchCompleteInspection,
} from "@/api/inspectionManagement/index.js";
import Pagination from "@/components/PIMTable/Pagination.vue";
  // 全局变量
  const { proxy } = getCurrentInstance();
  const formDia = ref();
  const viewFiles = ref();
  const uploadFiles = ref();
const { proxy } = getCurrentInstance();
const formDia = ref();
const viewFiles = ref();
const uploadFiles = ref();
  // 查询参数
  const queryParams = reactive({
    taskName: "",
  });
const queryParams = reactive({ taskName: "" });
  // 单选框配置
  const activeRadio = ref("taskManage");
  const radios = reactive([
    { name: "taskManage", label: "巡检任务" },
    { name: "task", label: "巡检记录" },
  ]);
const activeRadio = ref("taskManage");
const radios = reactive([
    { name: "taskManage", label: "巡检任务" },
    { name: "task", label: "巡检记录" },
]);
  // 表格数据
  const selectedRows = ref([]);
  const tableData = ref([]);
  const operationsArr = ref([]);
  const tableColumns = ref([]);
  const tableLoading = ref(false);
  const total = ref(0);
  const pageNum = ref(1);
  const pageSize = ref(10);
const selectedRows = ref([]);
const tableData = ref([]);
const operationsArr = ref([]);
const tableColumns = ref([]);
const tableLoading = ref(false);
const total = ref(0);
const pageNum = ref(1);
const pageSize = ref(100);
  // 列配置
  const columns = ref([
    { prop: "taskName", label: "巡检任务名称", minWidth: 200 },
    { prop: "inspectionProject", label: "巡检项目", minWidth: 180 },
    { prop: "remarks", label: "备注", minWidth: 180 },
    { prop: "inspector", label: "执行巡检人", minWidth: 180, slot: "inspector" },
    {
      prop: "isEnabled",
      label: "是否启用",
      minWidth: 100,
      dataType: "slot",
      slot: "isEnabled",
    },
    {
      prop: "frequencyType",
      label: "频次",
      minWidth: 120,
      formatData: params => {
        return params === "DAILY"
          ? "每日"
          : params === "WEEKLY"
          ? "每周"
          : params === "MONTHLY"
          ? "每月"
          : params === "QUARTERLY"
          ? "季度"
          : "";
      },
    },
    {
      prop: "frequencyDetail",
      label: "开始日期与时间",
      minWidth: 200,
      formatter: (row, column, cellValue) => {
        // 先判断是否是字符串
        if (typeof cellValue !== "string") return "";
        let val = cellValue;
        const replacements = {
          MON: "周一",
          TUE: "周二",
          WED: "周三",
          THU: "周四",
          FRI: "周五",
          SAT: "周六",
          SUN: "周日",
        };
        // 使用正则一次性替换所有匹配项
        return val.replace(
          /MON|TUE|WED|THU|FRI|SAT|SUN/g,
          match => replacements[match]
        );
      },
    },
    { prop: "registrant", label: "登记人", minWidth: 120 },
    {
      prop: "createTime",
      label: "登记日期",
      minWidth: 180,
      formatData: cell => {
        if (!cell) return "-";
        try {
          return dayjs(cell).format("YYYY-MM-DD HH:mm:ss");
        } catch {
          return cell;
        }
      },
    },
    // {
    //   prop: "inspectionResult",
    //   label: "巡检结果",
    //   minWidth: 100,
    //   dataType: "tag",
    //   formatData: val => {
    //     return val == 1 ? "正常" : "异常";
    //   },
    //   formatType: val => {
    //     return val == 1 ? "success" : "danger";
    //   },
    // },
    { prop: "abnormalDescription", label: "异常描述", minWidth: 150 },
  ]);
const columns = ref([
    { prop: "taskName", label: "巡检任务名称", minWidth: 200 },
    { prop: "inspectionProject", label: "巡检项目", minWidth: 180 },
    { prop: "remarks", label: "备注", minWidth: 180 },
    { prop: "inspector", label: "执行巡检人", minWidth: 180, slot: "inspector" },
    { prop: "isEnabled", label: "是否启用", minWidth: 100, dataType: "slot", slot: "isEnabled" },
    {
        prop: "frequencyType", label: "频次", minWidth: 120,
        formatData: params => ({ DAILY: "每日", WEEKLY: "每周", MONTHLY: "每月", QUARTERLY: "季度" }[params] || ""),
    },
    {
        prop: "frequencyDetail", label: "开始日期与时间", minWidth: 200,
        formatter: (row, column, cellValue) => {
            if (typeof cellValue !== "string") return "";
            return cellValue.replace(/MON|TUE|WED|THU|FRI|SAT|SUN/g, match => ({ MON: "周一", TUE: "周二", WED: "周三", THU: "周四", FRI: "周五", SAT: "周六", SUN: "周日" }[match]));
        },
    },
    { prop: "registrant", label: "登记人", minWidth: 120 },
    {
        prop: "createTime", label: "登记日期", minWidth: 180,
        formatData: cell => { if (!cell) return "-"; try { return dayjs(cell).format("YYYY-MM-DD HH:mm:ss"); } catch { return cell; } },
    },
    { prop: "abnormalDescription", label: "异常描述", minWidth: 150 },
]);
  // 操作列配置
  const getOperationColumn = operations => {
    if (!operations || operations.length === 0) return null;
const getOperationColumn = operations => {
    if (!operations || operations.length === 0) return null;
    return {
        label: "操作", width: operations.length > 1 ? 180 : 130, fixed: "right", align: "center", dataType: "action",
        operation: operations.map(op => {
            switch (op) {
                case "edit": return { name: "编辑", clickFun: handleAdd, color: "#409EFF" };
                case "upload": return { name: "上传", clickFun: openUploadDialog, color: "#409EFF" };
                case "viewFile": return { name: "查看附件", clickFun: viewFile, color: "#67C23A" };
                default: return null;
            }
        }).filter(Boolean),
    };
};
    const operationConfig = {
      label: "操作",
      width: operations.length > 1 ? 180 : 130,
      fixed: "right",
      align: "center",
      dataType: "action",
      operation: operations
        .map(op => {
          switch (op) {
            case "edit":
              return {
                name: "编辑",
                clickFun: handleAdd,
                color: "#409EFF",
              };
            case "upload":
              return {
                name: "上传",
                clickFun: openUploadDialog,
                color: "#409EFF",
              };
            case "viewFile":
              return {
                name: "查看附件",
                clickFun: viewFile,
                color: "#67C23A",
              };
            default:
              return null;
          }
        })
        .filter(Boolean),
    };
// 巡检记录 - 日期分组
const inspectionExpandedRowKeys = ref([]);
    return operationConfig;
  };
const groupInspectionByDate = (list) => {
    const map = {};
    list.forEach(item => {
        const date = item.dateStr || (item.createTime ? dayjs(item.createTime).format('YYYY-MM-DD') : '未知日期');
        if (!map[date]) map[date] = [];
        map[date].push(item);
    });
    return Object.entries(map).map(([date, children]) => ({ date, children, count: children.length }));
};
  onMounted(() => {
    radioChange("taskManage");
  });
const groupedInspectionList = computed(() => groupInspectionByDate(tableData.value));
  // 单选变化
  const radioChange = value => {
    if (value === "taskManage") {
      const operationColumn = getOperationColumn(["edit"]);
      tableColumns.value = [
        ...columns.value,
        ...(operationColumn ? [operationColumn] : []),
      ];
      operationsArr.value = ["edit"];
    } else if (value === "task") {
      const operationColumn = getOperationColumn(["upload", "viewFile"]);
      // 巡检记录不展示"是否启用"列
      const taskColumns = columns.value.filter(col => col.prop !== "isEnabled");
      tableColumns.value = [
        ...taskColumns,
        ...(operationColumn ? [operationColumn] : []),
      ];
      operationsArr.value = ["upload", "viewFile"];
    }
    pageNum.value = 1;
    pageSize.value = 10;
    getList();
  };
const onInspectionExpandChange = (row, expandedRows) => {
    inspectionExpandedRowKeys.value = expandedRows.map(r => r.date);
};
  // 查询操作
  const handleQuery = () => {
    pageNum.value = 1;
    pageSize.value = 10;
    getList();
  };
  // 分页处理
  const handlePagination = val => {
    pageNum.value = val.page;
    pageSize.value = val.limit;
    getList();
  };
  // 获取列表数据
  const getList = () => {
    tableLoading.value = true;
// 单条巡检完成
const handleSingleComplete = async (row) => {
    try {
        await ElMessageBox.confirm('确认将该巡检记录标记为完成?', '提示', { type: 'warning' });
        const res = await completeInspection(row.id);
        if (res.code === 200) {
            ElMessage.success('巡检完成');
            getList();
        } else {
            ElMessage.error(res.msg || '操作失败');
        }
    } catch { /* 取消 */ }
};
    const params = {
      ...queryParams,
      size: pageSize.value,
      current: pageNum.value,
    };
// 一键巡检完成
const handleBatchInspection = async (dateStr) => {
    try {
        await ElMessageBox.confirm(`确认将 ${dateStr} 的所有待巡检记录标记为完成?`, '提示', { type: 'warning' });
        const res = await batchCompleteInspection(dateStr);
        if (res.code === 200) {
            ElMessage.success(res.msg || '批量完成成功');
            getList();
        } else {
            ElMessage.error(res.msg || '操作失败');
        }
    } catch { /* 取消 */ }
};
    let apiCall;
    if (activeRadio.value === "task") {
      apiCall = inspectionTaskList(params);
    } else {
      apiCall = timingTaskList(params);
    }
onMounted(() => { radioChange("taskManage"); });
    apiCall
      .then(res => {
        const rawData = res.data.records || [];
        // 处理 inspector 字段,将字符串转换为数组(适用于所有情况)
        tableData.value = rawData.map(item => {
          const processedItem = { ...item };
          processedItem.__raw = { ...item };
const radioChange = value => {
    inspectionExpandedRowKeys.value = [];
    if (value === "taskManage") {
        const operationColumn = getOperationColumn(["edit"]);
        tableColumns.value = [...columns.value, ...(operationColumn ? [operationColumn] : [])];
        operationsArr.value = ["edit"];
    } else if (value === "task") {
        const operationColumn = getOperationColumn(["upload", "viewFile"]);
        const taskColumns = columns.value.filter(col => col.prop !== "isEnabled");
        tableColumns.value = [...taskColumns, ...(operationColumn ? [operationColumn] : [])];
        operationsArr.value = ["upload", "viewFile"];
    }
    pageNum.value = 1;
    getList();
};
          // 处理 inspector 字段
          if (processedItem.inspector) {
            if (typeof processedItem.inspector === "string") {
              // 字符串按逗号分割
              processedItem.inspector = processedItem.inspector
                .split(",")
                .map(s => s.trim())
                .filter(s => s);
            } else if (!Array.isArray(processedItem.inspector)) {
              // 非数组转为数组
              processedItem.inspector = [processedItem.inspector];
            }
          } else {
            // 空值设为空数组
            processedItem.inspector = [];
          }
const handleQuery = () => { pageNum.value = 1; getList(); };
          return processedItem;
        });
        total.value = res.data.total || 0;
      })
      .finally(() => {
        tableLoading.value = false;
      });
  };
const handlePagination = val => { pageNum.value = val.page; pageSize.value = val.limit; getList(); };
  // 重置查询
  const resetQuery = () => {
    for (const key in queryParams) {
      if (!["pageNum", "pageSize"].includes(key)) {
        queryParams[key] = "";
      }
    }
    handleQuery();
  };
const getList = () => {
    tableLoading.value = true;
    const params = { ...queryParams, size: pageSize.value, current: pageNum.value };
    const apiCall = activeRadio.value === "task" ? inspectionTaskList(params) : timingTaskList(params);
    apiCall.then(res => {
        const rawData = res.data.records || [];
        tableData.value = rawData.map(item => {
            const processedItem = { ...item };
            if (processedItem.inspector) {
                if (typeof processedItem.inspector === "string") {
                    processedItem.inspector = processedItem.inspector.split(",").map(s => s.trim()).filter(s => s);
                } else if (!Array.isArray(processedItem.inspector)) {
                    processedItem.inspector = [processedItem.inspector];
                }
            } else {
                processedItem.inspector = [];
            }
            return processedItem;
        });
        total.value = res.data.total || 0;
    }).finally(() => { tableLoading.value = false; });
};
  // 新增 / 编辑
  const handleAdd = row => {
    const type = row ? "edit" : "add";
    nextTick(() => {
      formDia.value?.openDialog(type, row);
    });
  };
const resetQuery = () => { queryParams.taskName = ""; handleQuery(); };
  // 查看附件
  const viewFile = row => {
    nextTick(() => {
      viewFiles.value?.openDialog(row);
    });
  };
const handleAdd = row => { nextTick(() => { formDia.value?.openDialog(row ? "edit" : "add", row); }); };
const viewFile = row => { nextTick(() => { viewFiles.value?.openDialog(row); }); };
const openUploadDialog = row => { nextTick(() => { uploadFiles.value?.openDialog(row); }); };
  const openUploadDialog = row => {
    nextTick(() => {
      uploadFiles.value?.openDialog(row);
    });
  };
const handleDelete = () => {
    if (!selectedRows.value.length) { proxy.$modal.msgWarning("请选择要删除的数据"); return; }
    proxy.$modal.confirm("是否确认删除所选数据项?").then(() => delTimingTask(selectedRows.value.map(item => item.id)))
        .then(() => { proxy.$modal.msgSuccess("删除成功"); handleQuery(); }).catch(() => {});
};
  // 删除操作
  const handleDelete = () => {
    if (!selectedRows.value.length) {
      proxy.$modal.msgWarning("请选择要删除的数据");
      return;
    }
const handleSelectionChange = selection => { selectedRows.value = selection; };
    const deleteIds = selectedRows.value.map(item => item.id);
    proxy.$modal
      .confirm("是否确认删除所选数据项?")
      .then(() => {
        return delTimingTask(deleteIds);
      })
      .then(() => {
        proxy.$modal.msgSuccess("删除成功");
        handleQuery();
      })
      .catch(() => {});
  };
  // 多选变更
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
  // 导出
  const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        // 根据当前选中的标签页调用不同的导出接口
        if (activeRadio.value === "taskManage") {
          // 巡检任务
          proxy.download("/timingTask/export", {}, "巡检任务.xlsx");
        } else if (activeRadio.value === "task") {
          // 巡检记录
          proxy.download("/inspectionTask/export", {}, "巡检记录.xlsx");
        }
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", { confirmButtonText: "确认", cancelButtonText: "取消", type: "warning" })
        .then(() => {
            if (activeRadio.value === "taskManage") proxy.download("/timingTask/export", {}, "巡检任务.xlsx");
            else proxy.download("/inspectionTask/export", {}, "巡检记录.xlsx");
        }).catch(() => { proxy.$modal.msg("已取消"); });
};
</script>
<style scoped>
  .person-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
  }
  .person-tag {
    margin-right: 4px;
    margin-bottom: 2px;
  }
  .no-data {
    color: #909399;
    font-size: 14px;
  }
.person-tags { display: flex; flex-wrap: wrap; gap: 4px; }
.person-tag { margin-right: 4px; margin-bottom: 2px; }
.no-data { color: #909399; font-size: 14px; }
</style>
src/views/equipmentManagement/spareParts/index.vue
@@ -1,26 +1,26 @@
<template>
  <div class="spare-part-category">
    <el-tabs v-model="activeTab" @tab-change="handleTabChange">
      <el-tab-pane label="备件列表" name="list">
        <div class="search_form">
          <el-form :inline="true" :model="queryParams" class="search-form">
            <el-form-item label="备件名称">
              <el-input
                v-model="queryParams.name"
                placeholder="请输入备件名称"
                clearable
                style="width: 240px"
              />
            </el-form-item>
            <el-form-item>
              <el-button type="primary" @click="handleQuery">查询</el-button>
              <el-button @click="resetQuery">重置</el-button>
            </el-form-item>
          </el-form>
          <div>
            <el-button type="primary" @click="addCategory">新增</el-button>
          </div>
        </div>
    <div class="spare-part-category">
        <el-tabs v-model="activeTab" @tab-change="handleTabChange">
            <el-tab-pane label="备件列表" name="list">
                <div class="search_form">
                    <el-form :inline="true" :model="queryParams" class="search-form">
                        <el-form-item label="备件名称">
                            <el-input
                                v-model="queryParams.name"
                                placeholder="请输入备件名称"
                                clearable
                                style="width: 240px"
                            />
                        </el-form-item>
                        <el-form-item>
                            <el-button type="primary" @click="handleQuery">查询</el-button>
                            <el-button @click="resetQuery">重置</el-button>
                        </el-form-item>
                    </el-form>
                    <div>
                        <el-button type="primary" @click="addCategory">新增</el-button>
                    </div>
                </div>
                <div class="table_list">
                    <PIMTable
                        rowKey="id"
@@ -36,83 +36,87 @@
                        </template>
                    </PIMTable>
                </div>
        <el-dialog title="分类管理" v-model="dialogVisible" width="60%">
          <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
            <el-form-item label="设备" prop="deviceLedgerIds">
              <el-select
                v-model="form.deviceLedgerIds"
                placeholder="请选择设备"
                filterable
                default-first-option
                :reserve-keyword="false"
                multiple
                style="width: 100%"
              >
                <el-option
                  v-for="(item, index) in deviceOptions"
                  :key="index"
                  :label="item.deviceName"
                  :value="item.id"
                ></el-option>
              </el-select>
            </el-form-item>
            <el-form-item label="备件名称" prop="name">
              <el-input v-model="form.name"></el-input>
            </el-form-item>
            <el-form-item label="备件编号" prop="sparePartsNo">
              <el-input v-model="form.sparePartsNo"></el-input>
            </el-form-item>
            <el-form-item label="数量" prop="quantity">
              <el-input type="number" v-model="form.quantity"></el-input>
            </el-form-item>
            <el-form-item label="状态" prop="status">
              <el-select v-model="form.status" placeholder="请选择状态">
                <el-option label="正常" value="正常"></el-option>
                <el-option label="禁用" value="禁用"></el-option>
              </el-select>
            </el-form-item>
            <el-form-item label="描述" prop="description">
              <el-input v-model="form.description"></el-input>
            </el-form-item>
            <el-form-item label="价格" prop="price">
              <el-input-number
                v-model="form.price"
                placeholder="请输入价格"
                :min="0"
                :step="0.01"
                :precision="2"
                style="width: 100%"
              ></el-input-number>
            </el-form-item>
          </el-form>
          <template #footer>
                <el-dialog title="分类管理" v-model="dialogVisible" width="60%">
                    <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
                        <el-form-item label="设备" prop="deviceLedgerIds">
                            <el-select
                                v-model="form.deviceLedgerIds"
                                placeholder="请选择设备"
                                filterable
                                default-first-option
                                :reserve-keyword="false"
                                multiple
                                style="width: 100%"
                            >
                                <el-option
                                    v-for="(item, index) in deviceOptions"
                                    :key="index"
                                    :label="item.deviceName"
                                    :value="item.id"
                                ></el-option>
                            </el-select>
                        </el-form-item>
                        <el-form-item label="备件名称" prop="name">
                            <el-input v-model="form.name"></el-input>
                        </el-form-item>
                        <el-form-item label="备件编号" prop="sparePartsNo">
                            <el-input v-model="form.sparePartsNo"></el-input>
                        </el-form-item>
                        <el-form-item label="数量" prop="quantity">
                            <el-input type="number" v-model="form.quantity"></el-input>
                        </el-form-item>
                        <el-form-item label="状态" prop="status">
                            <el-select v-model="form.status" placeholder="请选择状态">
                                <el-option label="正常" value="正常"></el-option>
                                <el-option label="禁用" value="禁用"></el-option>
                            </el-select>
                        </el-form-item>
                        <el-form-item label="描述" prop="description">
                            <el-input v-model="form.description"></el-input>
                        </el-form-item>
                        <el-form-item label="价格" prop="price">
                            <el-input-number
                                v-model="form.price"
                                placeholder="请输入价格"
                                :min="0"
                                :step="0.01"
                                :precision="2"
                                style="width: 100%"
                            ></el-input-number>
                        </el-form-item>
                    </el-form>
                    <template #footer>
            <span class="dialog-footer">
              <el-button type="primary" @click="submitForm" :loading="formLoading">确定</el-button>
              <el-button @click="dialogVisible = false" :disabled="formLoading">取消</el-button>
            </span>
          </template>
        </el-dialog>
      </el-tab-pane>
      <el-tab-pane label="备件领用记录" name="usage">
        <div class="search_form">
          <el-form :inline="true" :model="usageQuery" class="search-form">
            <el-form-item label="备件名称">
              <el-input v-model="usageQuery.sparePartsName" placeholder="请输入备件名称" clearable style="width: 240px" />
            </el-form-item>
            <el-form-item label="来源">
              <el-select v-model="usageQuery.sourceType" placeholder="请选择" clearable style="width: 200px">
                <el-option label="维修" :value="0" />
                <el-option label="保养" :value="1" />
              </el-select>
            </el-form-item>
            <el-form-item>
              <el-button type="primary" @click="handleUsageQuery">查询</el-button>
              <el-button @click="resetUsageQuery">重置</el-button>
            </el-form-item>
          </el-form>
        </div>
                    </template>
                </el-dialog>
            </el-tab-pane>
            <el-tab-pane label="备件领用记录" name="usage">
                <div class="search_form">
                    <el-form :inline="true" :model="usageQuery" class="search-form">
                        <el-form-item label="备件名称">
                            <el-input v-model="usageQuery.sparePartsName" placeholder="请输入备件名称" clearable style="width: 240px" />
                        </el-form-item>
                        <el-form-item label="来源">
                            <el-select v-model="usageQuery.sourceType" placeholder="请选择" clearable style="width: 200px">
                                <el-option label="维修" :value="0" />
                                <el-option label="保养" :value="1" />
                                <el-option label="直接出库" :value="2" />
                            </el-select>
                        </el-form-item>
                        <el-form-item>
                            <el-button type="primary" @click="handleUsageQuery">查询</el-button>
                            <el-button @click="resetUsageQuery">重置</el-button>
                        </el-form-item>
                    </el-form>
                    <div>
                        <el-button type="primary" @click="openRequisitionDialog">直接出库</el-button>
                    </div>
                </div>
                <div class="table_list">
                    <PIMTable
                        rowKey="rowKey"
@@ -124,9 +128,63 @@
                        @pagination="handleUsagePageChange"
                    />
                </div>
      </el-tab-pane>
    </el-tabs>
  </div>
            </el-tab-pane>
        </el-tabs>
        <!-- 直接出库弹窗 -->
        <el-dialog title="直接出库" v-model="requisitionDialogVisible" width="500px">
            <el-form :model="requisitionForm" :rules="requisitionRules" ref="requisitionFormRef" label-width="100px">
                <el-form-item label="设备选择" prop="deviceLedgerId">
                    <el-select
                        v-model="requisitionForm.deviceLedgerId"
                        placeholder="请选择设备"
                        filterable
                        clearable
                        style="width: 100%"
                    >
                        <el-option
                            v-for="item in requisitionDeviceOptions"
                            :key="item.id"
                            :label="item.deviceName"
                            :value="item.id"
                        />
                    </el-select>
                </el-form-item>
                <el-form-item label="备件选择" prop="sparePartsId">
                    <el-select
                        v-model="requisitionForm.sparePartsId"
                        placeholder="请选择备件"
                        filterable
                        clearable
                        style="width: 100%"
                        @change="onRequisitionSparePartChange"
                    >
                        <el-option
                            v-for="item in requisitionSparePartsOptions"
                            :key="item.id"
                            :label="`${item.name}(库存: ${item.quantity})`"
                            :value="item.id"
                        />
                    </el-select>
                </el-form-item>
                <el-form-item label="领用数量" prop="quantity">
                    <el-input-number
                        v-model="requisitionForm.quantity"
                        :min="1"
                        :max="selectedSparePartMaxQty"
                        style="width: 100%"
                    />
                </el-form-item>
                <el-form-item label="备注" prop="remark">
                    <el-input v-model="requisitionForm.remark" placeholder="请输入备注" />
                </el-form-item>
            </el-form>
            <template #footer>
                <el-button type="primary" @click="submitRequisition" :loading="requisitionLoading">确定</el-button>
                <el-button @click="requisitionDialogVisible = false">取消</el-button>
            </template>
        </el-dialog>
    </div>
</template>
<script setup>
@@ -135,7 +193,7 @@
import { getSparePartsList, addSparePart, editSparePart, delSparePart } from "@/api/equipmentManagement/spareParts";
import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import { getSparePartsUsagePage } from "@/api/equipmentManagement/sparePartsUsage";
import { getSparePartsUsagePage, outboundSpareParts } from "@/api/equipmentManagement/sparePartsUsage";
// 加载状态
const loading = ref(false);
@@ -157,375 +215,456 @@
const formRef = ref(null);
// 查询参数
const queryParams = reactive({
  name: ''
    name: ''
});
// 分页参数
const pagination = reactive({
  current: 1,
  size: 10,
  total: 0
    current: 1,
    size: 10,
    total: 0
});
// 备件领用记录
const usageLoading = ref(false);
const usageQuery = reactive({
  sparePartsName: "",
  sourceType: "",
    sparePartsName: "",
    sourceType: "",
});
const usagePagination = reactive({
  current: 1,
  size: 10,
  total: 0,
    current: 1,
    size: 10,
    total: 0,
});
const usageTableData = ref([]);
const usageColumns = ref([
  { label: "来源", prop: "sourceText" },
  { label: "单据/记录ID", prop: "sourceId" },
  { label: "设备名称", prop: "deviceName" },
  { label: "备件名称", prop: "sparePartsName" },
  { label: "领用数量", prop: "quantity" },
  { label: "操作人", prop: "operator" },
  { label: "时间", prop: "createTime" },
    { label: "来源", prop: "sourceText" },
    { label: "单据/记录ID", prop: "sourceId" },
    { label: "设备名称", prop: "deviceName" },
    { label: "备件名称", prop: "sparePartsName" },
    { label: "领用数量", prop: "quantity" },
    { label: "操作人", prop: "operator" },
    { label: "时间", prop: "createTime" },
]);
const handleTabChange = async (name) => {
  if (name === "usage") {
    usagePagination.current = 1;
    await fetchUsageData();
  }
    if (name === "usage") {
        usagePagination.current = 1;
        await fetchUsageData();
    }
};
const columns = ref([
  {
    label: "设备名称",
    prop: "deviceNameStr",
  },
  {
    label: "备件名称",
    prop: "name",
  },
  {
    label: "备件编号",
    prop: "sparePartsNo",
  },
  {
    label: "状态",
    prop: "status",
    slot: "status",
    dataType: "slot",
  },
  {
    label: "价格",
    prop: "price",
  },
  {
    label: "数量",
    prop: "quantity",
  },
  {
    label: "描述",
    prop: "description",
  },
  {
    label: "操作",
    prop: "operation",
    width: 150,
    fixed: 'right',
    align: "center",
    dataType: "action",
    operation: [
      {
        name: "编辑",
        clickFun: (row) => {
          editCategory(row)
        },
      },
      {
        name: "删除",
        clickFun: (row) => {
          deleteCategory(row.id)
        },
      },
    ],
  },
    {
        label: "设备名称",
        prop: "deviceNameStr",
    },
    {
        label: "备件名称",
        prop: "name",
    },
    {
        label: "备件编号",
        prop: "sparePartsNo",
    },
    {
        label: "状态",
        prop: "status",
        slot: "status",
        dataType: "slot",
    },
    {
        label: "价格",
        prop: "price",
    },
    {
        label: "数量",
        prop: "quantity",
    },
    {
        label: "描述",
        prop: "description",
    },
    {
        label: "操作",
        prop: "operation",
        width: 150,
        fixed: 'right',
        align: "center",
        dataType: "action",
        operation: [
            {
                name: "编辑",
                clickFun: (row) => {
                    editCategory(row)
                },
            },
            {
                name: "删除",
                clickFun: (row) => {
                    deleteCategory(row.id)
                },
            },
        ],
    },
]);
// 表单数据
const form = reactive({
  id:'',
  name: '',
  sparePartsNo: '',
  status: '',
  description: '',
  deviceLedgerIds: [],
  price: null
    id:'',
    name: '',
    sparePartsNo: '',
    status: '',
    description: '',
    deviceLedgerIds: [],
    price: null
});
// 表单验证规则
const rules = reactive({
  name: [
    { required: true, message: '请输入备件名称', trigger: 'blur' }
  ],
  sparePartsNo: [
    { required: true, message: '请输入备件编号', trigger: 'blur' }
  ],
  quantity:[
    { required: true, message: '请输入数量', trigger: 'blur' }
  ],
  status: [
    { required: true, message: '请选择状态', trigger: 'change' }
  ],
  deviceLedgerIds: [
    {
      required: true,
      message: '请选择设备',
      trigger: 'change',
      validator: (rule, value, callback) => {
        if (operationType.value === 'add' && (!value || value.length === 0)) {
          callback(new Error('请选择设备'));
        } else {
          callback();
        }
      }
    }
  ]
    name: [
        { required: true, message: '请输入备件名称', trigger: 'blur' }
    ],
    sparePartsNo: [
        { required: true, message: '请输入备件编号', trigger: 'blur' }
    ],
    quantity:[
        { required: true, message: '请输入数量', trigger: 'blur' }
    ],
    status: [
        { required: true, message: '请选择状态', trigger: 'change' }
    ],
    deviceLedgerIds: [
        {
            required: true,
            message: '请选择设备',
            trigger: 'change',
            validator: (rule, value, callback) => {
                if (operationType.value === 'add' && (!value || value.length === 0)) {
                    callback(new Error('请选择设备'));
                } else {
                    callback();
                }
            }
        }
    ]
});
// 获取缩进量
const getIndentation = (row) => {
  // 这里简单返回 20,可根据实际需求实现层级缩进逻辑
  return 20;
    // 这里简单返回 20,可根据实际需求实现层级缩进逻辑
    return 20;
};
// 定义 buildTree 函数
const buildTree = (flatData) => {
  const map = {};
  const result = [];
  if(flatData){
    return result;
  }
  flatData.forEach(item => {
    map[item.id] = { ...item, children: [] };
  });
  flatData.forEach(item => {
    if (item.parentId === null || !map[item.parentId]) {
      result.push(map[item.id]);
    } else {
      map[item.parentId].children.push(map[item.id]);
    }
  });
  return result;
    const map = {};
    const result = [];
    if(flatData){
        return result;
    }
    flatData.forEach(item => {
        map[item.id] = { ...item, children: [] };
    });
    flatData.forEach(item => {
        if (item.parentId === null || !map[item.parentId]) {
            result.push(map[item.id]);
        } else {
            map[item.parentId].children.push(map[item.id]);
        }
    });
    return result;
};
// 获取列表数据
const fetchListData = async () => {
  loading.value = true;
  try {
    const params = {
      current: pagination.current,
      size: pagination.size
    };
    if (queryParams.name) {
      params.name = queryParams.name;
    }
    const res = await getSparePartsList(params);
    if (res.code === 200) {
      renderTableData.value = res.data.records || [];
      categories.value = res.data.records || [];
      pagination.total = res.data.total || 0;
    }
  } catch (error) {
    loading.value = true;
    try {
        const params = {
            current: pagination.current,
            size: pagination.size
        };
        if (queryParams.name) {
            params.name = queryParams.name;
        }
        const res = await getSparePartsList(params);
        if (res.code === 200) {
            renderTableData.value = res.data.records || [];
            categories.value = res.data.records || [];
            pagination.total = res.data.total || 0;
        }
    } catch (error) {
        loading.value = false;
  } finally {
    loading.value = false;
  }
    } finally {
        loading.value = false;
    }
}
const fetchUsageData = async () => {
  usageLoading.value = true;
  try {
    const res = await getSparePartsUsagePage({
      current: usagePagination.current,
      size: usagePagination.size,
      sparePartsName: usageQuery.sparePartsName || undefined,
      sourceType: usageQuery.sourceType || undefined,
    });
    if (res?.code === 200) {
      const records = res?.data?.records || [];
      usagePagination.total = res?.data?.total || 0;
      usageTableData.value = records.map((r, idx) => ({
        rowKey: r.id ?? `${usagePagination.current}-${idx}`,
        ...r,
        sourceText: r.sourceText === "" ? "-" : r.sourceText,
      }));
    } else {
      usagePagination.total = 0;
      usageTableData.value = [];
    }
  } finally {
    usageLoading.value = false;
  }
    usageLoading.value = true;
    try {
        const res = await getSparePartsUsagePage({
            current: usagePagination.current,
            size: usagePagination.size,
            sparePartsName: usageQuery.sparePartsName || undefined,
            sourceType: usageQuery.sourceType || undefined,
        });
        if (res?.code === 200) {
            const records = res?.data?.records || [];
            usagePagination.total = res?.data?.total || 0;
            usageTableData.value = records.map((r, idx) => ({
                rowKey: r.id ?? `${usagePagination.current}-${idx}`,
                ...r,
                sourceText: r.sourceText === "" ? "-" : r.sourceText,
            }));
        } else {
            usagePagination.total = 0;
            usageTableData.value = [];
        }
    } finally {
        usageLoading.value = false;
    }
};
const handleUsageQuery = () => {
  usagePagination.current = 1;
  fetchUsageData();
    usagePagination.current = 1;
    fetchUsageData();
};
const resetUsageQuery = () => {
  usageQuery.sparePartsName = "";
  usageQuery.sourceType = "";
  usagePagination.current = 1;
  fetchUsageData();
    usageQuery.sparePartsName = "";
    usageQuery.sourceType = "";
    usagePagination.current = 1;
    fetchUsageData();
};
const handleUsagePageChange = (obj) => {
  usagePagination.current = obj.page;
  usagePagination.size = obj.limit;
  fetchUsageData();
    usagePagination.current = obj.page;
    usagePagination.size = obj.limit;
    fetchUsageData();
};
// 查询
const handleQuery = () => {
  pagination.current = 1;
  fetchListData();
    pagination.current = 1;
    fetchListData();
}
// 重置查询
const resetQuery = () => {
  queryParams.name = '';
  pagination.current = 1;
  fetchListData();
    queryParams.name = '';
    pagination.current = 1;
    fetchListData();
}
// 分页大小改变
const handleSizeChange = (size) => {
  pagination.size = size;
  pagination.current = 1;
  fetchListData();
}
// 当前页改变
const handleCurrentChange = (current) => {
  pagination.current = current;
  fetchListData();
// 分页变化
const handleSizeChange = ({ page, limit }) => {
    pagination.current = page;
    pagination.size = limit;
    fetchListData();
}
// 加载设备列表(在打开弹框时调用)
const loadDeviceName = async () => {
  try {
    const { data } = await getDeviceLedger();
    deviceOptions.value = data || [];
  } catch (error) {
    ElMessage.error('获取设备列表失败');
  }
    try {
        const { data } = await getDeviceLedger();
        deviceOptions.value = data || [];
    } catch (error) {
        ElMessage.error('获取设备列表失败');
    }
};
// 新增分类
const addCategory = async () => {
  await loadDeviceName();
  form.id = '';
  form.name = '';
  form.sparePartsNo = '';
  form.status = '';
  form.description = '';
  form.deviceLedgerIds = [];
  form.quantity = undefined;
  form.price = null;
  operationType.value = 'add'
  dialogVisible.value = true;
    await loadDeviceName();
    form.id = '';
    form.name = '';
    form.sparePartsNo = '';
    form.status = '';
    form.description = '';
    form.deviceLedgerIds = [];
    form.quantity = undefined;
    form.price = null;
    operationType.value = 'add'
    dialogVisible.value = true;
};
// 编辑分类
const editCategory = async (row) => {
  await loadDeviceName();
  Object.assign(form, row);
  // 如果后端返回的是 deviceIds 字符串,需要转换为数组
  if (row.deviceIds && typeof row.deviceIds === 'string') {
    // 确保ID类型与设备选项中的ID类型一致
    const deviceIdsArray = row.deviceIds.split(',').map(id => id.trim()).filter(id => id);
    // 如果设备选项中的ID是数字类型,则转换为数字
    if (deviceOptions.value.length > 0 && typeof deviceOptions.value[0].id === 'number') {
      form.deviceLedgerIds = deviceIdsArray.map(id => Number(id)).filter(id => !isNaN(id));
    } else {
      form.deviceLedgerIds = deviceIdsArray;
    }
  } else if (row.deviceIds && Array.isArray(row.deviceIds)) {
    form.deviceLedgerIds = row.deviceIds;
  } else {
    form.deviceLedgerIds = [];
  }
  operationType.value = 'edit'
  dialogVisible.value = true;
    await loadDeviceName();
    Object.assign(form, row);
    // 如果后端返回的是 deviceIds 字符串,需要转换为数组
    if (row.deviceIds && typeof row.deviceIds === 'string') {
        // 确保ID类型与设备选项中的ID类型一致
        const deviceIdsArray = row.deviceIds.split(',').map(id => id.trim()).filter(id => id);
        // 如果设备选项中的ID是数字类型,则转换为数字
        if (deviceOptions.value.length > 0 && typeof deviceOptions.value[0].id === 'number') {
            form.deviceLedgerIds = deviceIdsArray.map(id => Number(id)).filter(id => !isNaN(id));
        } else {
            form.deviceLedgerIds = deviceIdsArray;
        }
    } else if (row.deviceIds && Array.isArray(row.deviceIds)) {
        form.deviceLedgerIds = row.deviceIds;
    } else {
        form.deviceLedgerIds = [];
    }
    operationType.value = 'edit'
    dialogVisible.value = true;
};
// 删除分类
const deleteCategory = async (id) => {
  try {
    await ElMessageBox.confirm('此操作将永久删除该分类,是否继续?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    });
    loading.value = true;
    const res = await delSparePart(id);
    if (res.code === 200) {
      ElMessage.success('删除成功');
      fetchListData();
    } else {
      ElMessage.error(res.message || '删除失败');
    }
  } catch (error) {
    if (error !== 'cancel') {
      ElMessage.error('删除失败');
    }
  } finally {
    loading.value = false;
  }
    try {
        await ElMessageBox.confirm('此操作将永久删除该分类,是否继续?', '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
        });
        loading.value = true;
        const res = await delSparePart(id);
        if (res.code === 200) {
            ElMessage.success('删除成功');
            fetchListData();
        } else {
            ElMessage.error(res.message || '删除失败');
        }
    } catch (error) {
        if (error !== 'cancel') {
            ElMessage.error('删除失败');
        }
    } finally {
        loading.value = false;
    }
};
// 提交表单
const submitForm = async () => {
  if (!formRef.value) return;
  try {
    await formRef.value.validate();
    formLoading.value = true;
    // 构建提交数据
    const submitData = {
      ...form,
      deviceIds: form.deviceLedgerIds && form.deviceLedgerIds.length > 0
        ? form.deviceLedgerIds.join(',')
        : ''
    };
    // 删除不需要的字段
    delete submitData.deviceLedgerIds;
    if (operationType.value === 'edit') {
      let res = await editSparePart(submitData);
      if (res.code === 200) {
        ElMessage.success('编辑成功');
        dialogVisible.value = false;
        fetchListData();
      }
    } else {
      let res = await addSparePart(submitData);
      if (res.code === 200) {
        ElMessage.success('新增成功');
        dialogVisible.value = false;
        fetchListData();
      }
    }
  } catch (error) {
    ElMessage.error('请填写完整表单信息');
  } finally {
    formLoading.value = false;
  }
    if (!formRef.value) return;
    try {
        await formRef.value.validate();
        formLoading.value = true;
        // 构建提交数据
        const submitData = {
            ...form,
            deviceIds: form.deviceLedgerIds && form.deviceLedgerIds.length > 0
                ? form.deviceLedgerIds.join(',')
                : ''
        };
        // 删除不需要的字段
        delete submitData.deviceLedgerIds;
        if (operationType.value === 'edit') {
            let res = await editSparePart(submitData);
            if (res.code === 200) {
                ElMessage.success('编辑成功');
                dialogVisible.value = false;
                fetchListData();
            }
        } else {
            let res = await addSparePart(submitData);
            if (res.code === 200) {
                ElMessage.success('新增成功');
                dialogVisible.value = false;
                fetchListData();
            }
        }
    } catch (error) {
        ElMessage.error('请填写完整表单信息');
    } finally {
        formLoading.value = false;
    }
};
// 直接出库
const requisitionDialogVisible = ref(false);
const requisitionLoading = ref(false);
const requisitionFormRef = ref(null);
const requisitionDeviceOptions = ref([]);
const requisitionSparePartsOptions = ref([]);
const selectedSparePartMaxQty = ref(1);
const requisitionForm = reactive({
    deviceLedgerId: null,
    sparePartsId: null,
    quantity: 1,
    remark: '',
});
const requisitionRules = {
    sparePartsId: [{ required: true, message: '请选择备件', trigger: 'change' }],
    quantity: [{ required: true, message: '请输入领用数量', trigger: 'blur' }],
};
const loadRequisitionOptions = async () => {
    try {
        const [deviceRes, spareRes] = await Promise.all([
            getDeviceLedger(),
            getSparePartsList({ current: 1, size: 9999 }),
        ]);
        requisitionDeviceOptions.value = deviceRes.data || [];
        requisitionSparePartsOptions.value = spareRes.data?.records || [];
    } catch (e) {
        ElMessage.error('加载选项失败');
    }
};
const openRequisitionDialog = async () => {
    requisitionForm.deviceLedgerId = null;
    requisitionForm.sparePartsId = null;
    requisitionForm.quantity = 1;
    requisitionForm.remark = '';
    selectedSparePartMaxQty.value = 1;
    await loadRequisitionOptions();
    requisitionDialogVisible.value = true;
};
const onRequisitionSparePartChange = (val) => {
    const selected = requisitionSparePartsOptions.value.find(item => item.id === val);
    selectedSparePartMaxQty.value = selected ? selected.quantity : 1;
    if (requisitionForm.quantity > selectedSparePartMaxQty.value) {
        requisitionForm.quantity = selectedSparePartMaxQty.value;
    }
};
const submitRequisition = async () => {
    if (!requisitionFormRef.value) return;
    try {
        await requisitionFormRef.value.validate();
    } catch {
        return;
    }
    if (requisitionForm.quantity <= 0) {
        ElMessage.warning('领用数量必须大于0');
        return;
    }
    const selected = requisitionSparePartsOptions.value.find(item => item.id === requisitionForm.sparePartsId);
    if (selected && requisitionForm.quantity > selected.quantity) {
        ElMessage.warning(`库存不足,当前库存为 ${selected.quantity}`);
        return;
    }
    requisitionLoading.value = true;
    try {
        const res = await outboundSpareParts({
            deviceLedgerId: requisitionForm.deviceLedgerId || undefined,
            sparePartsId: requisitionForm.sparePartsId,
            quantity: requisitionForm.quantity,
            remark: requisitionForm.remark || undefined,
        });
        if (res.code === 200) {
            ElMessage.success('领用成功');
            requisitionDialogVisible.value = false;
            fetchUsageData();
        } else {
            ElMessage.error(res.msg || '领用失败');
        }
    } catch {
        ElMessage.error('领用失败');
    } finally {
        requisitionLoading.value = false;
    }
};
// 组件挂载时获取列表数据
onMounted(() => {
  fetchListData();
    fetchListData();
});
</script>
<style scoped>
.spare-part-category {
  padding: 20px;
    padding: 20px;
}
.search_form {
    display: flex;
@@ -533,38 +672,38 @@
    justify-content: space-between;
}
.table_list {
  margin-top: unset;
    margin-top: unset;
}
.pagination-container {
  margin-top: 20px;
  display: flex;
  justify-content: flex-end;
    margin-top: 20px;
    display: flex;
    justify-content: flex-end;
}
.el-table__header-wrapper th {
  background-color: #f5f7fa;
  font-weight: 600;
    background-color: #f5f7fa;
    font-weight: 600;
}
.el-table__row:hover > td {
  background-color: #fafafa;
    background-color: #fafafa;
}
/* 按钮组样式 */
.actions > div {
  display: flex;
  gap: 10px;
    display: flex;
    gap: 10px;
}
/* 确保表格中的操作按钮不会被截断 */
.el-table-column--fixed-right .el-button {
  margin: 0 2px;
    margin: 0 2px;
}
/* 树形节点内容样式 */
.nested-tree .el-tree-node__expand-icon {
  font-size: 12px;
  margin-right: 4px;
    font-size: 12px;
    margin-right: 4px;
}
</style>
src/views/equipmentManagement/upkeep/index.vue
@@ -1,652 +1,726 @@
<template>
  <div class="app-container">
    <el-tabs v-model="activeTab"
             @tab-change="handleTabChange">
      <!-- 保养任务tab -->
      <el-tab-pane label="保养任务"
                   name="scheduled">
        <div class="search_form">
          <el-form :model="scheduledFilters"
                   :inline="true">
            <el-form-item label="任务名称">
              <el-input v-model="scheduledFilters.taskName"
                        style="width: 240px"
                        placeholder="请输入任务名称"
                        clearable
                        :prefix-icon="Search"
                        @change="getScheduledTableData" />
            </el-form-item>
            <el-form-item label="任务状态">
              <el-select v-model="scheduledFilters.status"
                         placeholder="请选择任务状态"
                         clearable
                         style="width: 200px">
                <el-option label="启用"
                           value="1" />
                <el-option label="停用"
                           value="0" />
              </el-select>
            </el-form-item>
            <el-form-item>
              <el-button type="primary"
                         @click="getScheduledTableData">搜索</el-button>
              <el-button @click="resetScheduledFilters">重置</el-button>
            </el-form-item>
          </el-form>
        </div>
        <div class="table_list">
          <div class="actions">
            <el-text class="mx-1"
                     size="large">保养任务</el-text>
            <div>
              <el-button type="primary"
                         icon="Plus"
                         @click="addScheduledTask">
                新增任务
              </el-button>
              <el-button type="danger"
                         icon="Delete"
                         :disabled="scheduledMultipleList.length <= 0"
                         @click="delScheduledTaskByIds(scheduledMultipleList.map((item) => item.id))">
                批量删除
              </el-button>
            </div>
          </div>
          <PIMTable rowKey="id"
                    isSelection
                    :column="scheduledColumns"
                    :tableData="scheduledDataList"
                    :page="{
    <div class="app-container">
        <el-tabs v-model="activeTab"
                 @tab-change="handleTabChange">
            <!-- 保养任务tab -->
            <el-tab-pane label="保养任务"
                         name="scheduled">
                <div class="search_form">
                    <el-form :model="scheduledFilters"
                             :inline="true">
                        <el-form-item label="任务名称">
                            <el-input v-model="scheduledFilters.taskName"
                                      style="width: 240px"
                                      placeholder="请输入任务名称"
                                      clearable
                                      :prefix-icon="Search"
                                      @change="getScheduledTableData" />
                        </el-form-item>
                        <el-form-item label="任务状态">
                            <el-select v-model="scheduledFilters.status"
                                       placeholder="请选择任务状态"
                                       clearable
                                       style="width: 200px">
                                <el-option label="启用"
                                           value="1" />
                                <el-option label="停用"
                                           value="0" />
                            </el-select>
                        </el-form-item>
                        <el-form-item>
                            <el-button type="primary"
                                       @click="getScheduledTableData">搜索</el-button>
                            <el-button @click="resetScheduledFilters">重置</el-button>
                        </el-form-item>
                    </el-form>
                </div>
                <div class="table_list">
                    <div class="actions">
                        <el-text class="mx-1"
                                 size="large">保养任务</el-text>
                        <div>
                            <el-button type="primary"
                                       icon="Plus"
                                       @click="addScheduledTask">
                                新增任务
                            </el-button>
                            <el-button type="danger"
                                       icon="Delete"
                                       :disabled="scheduledMultipleList.length <= 0"
                                       @click="delScheduledTaskByIds(scheduledMultipleList.map((item) => item.id))">
                                批量删除
                            </el-button>
                        </div>
                    </div>
                    <PIMTable rowKey="id"
                              isSelection
                              :column="scheduledColumns"
                              :tableData="scheduledDataList"
                              :page="{
              current: scheduledPagination.currentPage,
              size: scheduledPagination.pageSize,
              total: scheduledPagination.total,
            }"
                    @selection-change="handleScheduledSelectionChange"
                    @pagination="changeScheduledPage">
            <template #statusRef="{ row }">
              <el-tag v-if="row.status === 1"
                      type="success">启用</el-tag>
              <el-tag v-if="row.status === 0"
                      type="danger">停用</el-tag>
            </template>
            <template #operation="{ row }">
              <el-button type="primary"
                         link
                         @click="editScheduledTask(row)">
                编辑
              </el-button>
              <el-button type="danger"
                         link
                         @click="delScheduledTaskByIds(row.id)">
                删除
              </el-button>
            </template>
          </PIMTable>
        </div>
      </el-tab-pane>
      <!-- 保养记录tab(原设备保养页面) -->
      <el-tab-pane label="保养记录"
                   name="record">
        <div class="search_form">
          <el-form :model="filters"
                   :inline="true">
            <el-form-item label="设备名称">
              <el-input v-model="filters.deviceName"
                        style="width: 240px"
                        placeholder="请输入设备名称"
                        clearable
                        :prefix-icon="Search"
                        @change="getTableData" />
            </el-form-item>
            <el-form-item label="计划保养日期">
              <el-date-picker v-model="filters.maintenancePlanTime"
                              type="date"
                              placeholder="请选择计划保养日期"
                              size="default"
                              @change="(date) => handleDateChange(date,2)" />
            </el-form-item>
            <el-form-item label="实际保养日期">
              <el-date-picker v-model="filters.maintenanceActuallyTime"
                              type="date"
                              placeholder="请选择实际保养日期"
                              size="default"
                              @change="(date) => handleDateChange(date,1)" />
            </el-form-item>
            <el-form-item label="实际保养人">
              <el-input v-model="filters.maintenanceActuallyName"
                        style="width: 240px"
                        placeholder="请输入实际保养人"
                        clearable
                        :prefix-icon="Search"
                        @change="getTableData" />
            </el-form-item>
            <el-form-item>
              <el-button type="primary"
                         @click="getTableData">搜索</el-button>
              <el-button @click="resetFilters">重置</el-button>
            </el-form-item>
          </el-form>
        </div>
        <div class="table_list">
          <div class="actions">
            <el-text class="mx-1"
                     size="large">保养记录</el-text>
            <div>
              <el-button type="success"
                         icon="Van"
                         @click="addPlan">
                新增计划
              </el-button>
              <el-button @click="handleOut">
                导出
              </el-button>
              <el-button type="danger"
                         icon="Delete"
                         :disabled="multipleList.length <= 0 || hasFinishedStatus"
                         @click="delRepairByIds(multipleList.map((item) => item.id))">
                批量删除
              </el-button>
            </div>
          </div>
          <PIMTable rowKey="id"
                    isSelection
                    :column="columns"
                    :tableData="dataList"
                    :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
                    @selection-change="handleSelectionChange"
                    @pagination="changePage">
            <template #maintenanceResultRef="{ row }">
              <div>{{ row.maintenanceResult || '-' }}</div>
            </template>
            <template #statusRef="{ row }">
              <el-tag v-if="row.status === 2"
                      type="danger">失败</el-tag>
              <el-tag v-if="row.status === 1"
                      type="success">完结</el-tag>
              <el-tag v-if="row.status === 0"
                      type="warning">待保养</el-tag>
            </template>
            <template #operation="{ row }">
              <!-- 这个功能跟新增保养功能一模一样,有啥意义? -->
              <!-- <el-button
              type="primary"
              text
              @click="addMaintain(row)"
          >
            新增保养
          </el-button> -->
              <el-button type="primary"
                         link
                         :disabled="row.status === 1"
                         @click="editPlan(row.id)">
                编辑
              </el-button>
              <el-button type="success"
                         link
                         :disabled="row.status === 1"
                         @click="addMaintain(row)">
                保养
              </el-button>
              <el-button type="danger"
                         link
                         :disabled="row.status === 1"
                         @click="delRepairByIds(row.id)">
                删除
              </el-button>
              <el-button type="primary"
                         link
                         @click="openFileDialog(row)">
                附件
              </el-button>
            </template>
          </PIMTable>
        </div>
      </el-tab-pane>
    </el-tabs>
    <PlanModal ref="planModalRef"
               @ok="getTableData" />
    <MaintenanceModal ref="maintainModalRef"
                      @ok="getTableData" />
    <FormDia ref="formDiaRef"
             @closeDia="getScheduledTableData" />
    <FileList v-if="fileDialogVisible"
              v-model:visible="fileDialogVisible"
              :record-type="'device_maintenance'"
              :record-id="currentMaintenanceTaskId" />
  </div>
                              @selection-change="handleScheduledSelectionChange"
                              @pagination="changeScheduledPage">
                        <template #statusRef="{ row }">
                            <el-tag v-if="row.status === 1"
                                    type="success">启用</el-tag>
                            <el-tag v-if="row.status === 0"
                                    type="danger">停用</el-tag>
                        </template>
                        <template #operation="{ row }">
                            <el-button type="primary"
                                       link
                                       @click="editScheduledTask(row)">
                                编辑
                            </el-button>
                            <el-button type="danger"
                                       link
                                       @click="delScheduledTaskByIds(row.id)">
                                删除
                            </el-button>
                        </template>
                    </PIMTable>
                </div>
            </el-tab-pane>
            <!-- 保养记录tab(原设备保养页面) -->
            <el-tab-pane label="保养记录"
                         name="record">
                <div class="search_form">
                    <el-form :model="filters"
                             :inline="true">
                        <el-form-item label="设备名称">
                            <el-input v-model="filters.deviceName"
                                      style="width: 240px"
                                      placeholder="请输入设备名称"
                                      clearable
                                      :prefix-icon="Search"
                                      @change="getTableData" />
                        </el-form-item>
                        <el-form-item label="计划保养日期">
                            <el-date-picker v-model="filters.maintenancePlanTime"
                                            type="date"
                                            placeholder="请选择计划保养日期"
                                            size="default"
                                            @change="(date) => handleDateChange(date,2)" />
                        </el-form-item>
                        <el-form-item label="实际保养日期">
                            <el-date-picker v-model="filters.maintenanceActuallyTime"
                                            type="date"
                                            placeholder="请选择实际保养日期"
                                            size="default"
                                            @change="(date) => handleDateChange(date,1)" />
                        </el-form-item>
                        <el-form-item label="实际保养人">
                            <el-input v-model="filters.maintenanceActuallyName"
                                      style="width: 240px"
                                      placeholder="请输入实际保养人"
                                      clearable
                                      :prefix-icon="Search"
                                      @change="getTableData" />
                        </el-form-item>
                        <el-form-item>
                            <el-button type="primary"
                                       @click="getTableData">搜索</el-button>
                            <el-button @click="resetFilters">重置</el-button>
                        </el-form-item>
                    </el-form>
                </div>
                <div class="table_list">
                    <div class="actions">
                        <el-text class="mx-1"
                                 size="large">保养记录</el-text>
                        <div>
                            <el-button type="success"
                                       icon="Van"
                                       @click="addPlan">
                                新增计划
                            </el-button>
                            <el-button @click="handleOut">
                                导出
                            </el-button>
                            <el-button type="danger"
                                       icon="Delete"
                                       :disabled="multipleList.length <= 0"
                                       @click="delRepairByIds(multipleList.map((item) => item.id))">
                                批量删除
                            </el-button>
                        </div>
                    </div>
                    <el-table
                        :data="groupedDataList"
                        border
                        v-loading="tableLoading"
                        @selection-change="handleSelectionChange"
                        :expand-row-keys="expandedRowKeys"
                        :row-key="(row) => row.date"
                        @expand-change="onExpandChange"
                    >
                        <el-table-column type="selection" width="55" />
                        <el-table-column type="expand">
                            <template #default="props">
                                <el-table :data="props.row.children" border row-key="id">
                                    <el-table-column label="设备名称" prop="deviceName" align="center" />
                                    <el-table-column label="规格型号" prop="deviceModel" align="center" />
                                    <el-table-column label="录入人" prop="createUserName" align="center" />
                                    <el-table-column label="保养项目" prop="machineryCategory" align="center">
                                        <template #default="{ row }">
                                            {{ row.machineryCategory || '--' }}
                                        </template>
                                    </el-table-column>
                                    <el-table-column label="实际保养人" prop="maintenanceActuallyName" align="center" />
                                    <el-table-column label="实际保养日期" align="center">
                                        <template #default="{ row }">
                                            {{ row.maintenanceActuallyTime ? dayjs(row.maintenanceActuallyTime).format('YYYY-MM-DD HH:mm:ss') : '-' }}
                                        </template>
                                    </el-table-column>
                                    <el-table-column label="保养结果" align="center">
                                        <template #default="{ row }">
                                            {{ row.maintenanceResult || '-' }}
                                        </template>
                                    </el-table-column>
                                    <el-table-column label="状态" align="center">
                                        <template #default="{ row }">
                                            <el-tag v-if="row.status === 2" type="danger">失败</el-tag>
                                            <el-tag v-if="row.status === 1" type="success">完结</el-tag>
                                            <el-tag v-if="row.status === 0" type="warning">待保养</el-tag>
                                        </template>
                                    </el-table-column>
                                    <el-table-column label="操作" align="center" width="300">
                                        <template #default="{ row }">
                                            <el-button type="primary" link :disabled="row.status === 1" @click="editPlan(row.id)">编辑</el-button>
                                            <el-button type="success" link :disabled="row.status !== 0" @click="handleSingleComplete(row.id)">保养完成</el-button>
                                            <el-button type="success" link :disabled="row.status === 1" @click="addMaintain(row)">保养</el-button>
                                            <el-button type="danger" link :disabled="row.status === 1" @click="delRepairByIds(row.id)">删除</el-button>
                                            <el-button type="primary" link @click="openFileDialog(row)">附件</el-button>
                                        </template>
                                    </el-table-column>
                                </el-table>
                            </template>
                        </el-table-column>
                        <el-table-column label="计划保养日期" prop="date" align="center" />
                        <el-table-column label="保养记录数" prop="count" align="center" />
                        <el-table-column label="待保养数" align="center" width="120">
                            <template #default="{ row }">
                                {{ row.children.filter(c => c.status === 0).length }}
                            </template>
                        </el-table-column>
                        <el-table-column label="操作" align="center" width="200">
                            <template #default="{ row }">
                                <el-button
                                    type="primary"
                                    size="small"
                                    :disabled="!row.children.some(c => c.status === 0)"
                                    @click="handleBatchComplete(row.date)"
                                >
                                    一键保养完成
                                </el-button>
                            </template>
                        </el-table-column>
                    </el-table>
                    <Pagination
                        v-show="pagination.total > 0"
                        :total="pagination.total"
                        layout="total, sizes, prev, pager, next, jumper"
                        :page="pagination.currentPage"
                        :limit="pagination.pageSize"
                        @pagination="changePage"
                    />
                </div>
            </el-tab-pane>
        </el-tabs>
        <PlanModal ref="planModalRef"
                   @ok="getTableData" />
        <MaintenanceModal ref="maintainModalRef"
                          @ok="getTableData" />
        <FormDia ref="formDiaRef"
                 @closeDia="getScheduledTableData" />
        <FileList v-if="fileDialogVisible"
                  v-model:visible="fileDialogVisible"
                  :record-type="'device_maintenance'"
                  :record-id="currentMaintenanceTaskId" />
    </div>
</template>
<script setup>
  import {
    ref,
    onMounted,
    reactive,
    getCurrentInstance,
    nextTick,
    computed,
    defineAsyncComponent,
  } from "vue";
  import { Search } from "@element-plus/icons-vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import PlanModal from "./Form/PlanModal.vue";
  import MaintenanceModal from "./Form/MaintenanceModal.vue";
  import FormDia from "./Form/formDia.vue";
  import {
    getUpkeepPage,
    delUpkeep,
    deviceMaintenanceTaskList,
    deviceMaintenanceTaskDel,
  } from "@/api/equipmentManagement/upkeep";
  import dayjs from "dayjs";
import {
    ref,
    onMounted,
    reactive,
    getCurrentInstance,
    nextTick,
    computed,
    defineAsyncComponent,
} from "vue";
import { Search } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus";
import PlanModal from "./Form/PlanModal.vue";
import MaintenanceModal from "./Form/MaintenanceModal.vue";
import FormDia from "./Form/formDia.vue";
import {
    getUpkeepPage,
    delUpkeep,
    deviceMaintenanceTaskList,
    deviceMaintenanceTaskDel,
    batchCompleteMaintenance,
    completeMaintenance,
} from "@/api/equipmentManagement/upkeep";
import dayjs from "dayjs";
import Pagination from "@/components/PIMTable/Pagination.vue";
  const { proxy } = getCurrentInstance();
  const FileList = defineAsyncComponent(() =>
    import("@/components/Dialog/FileList.vue")
  );
const { proxy } = getCurrentInstance();
const FileList = defineAsyncComponent(() =>
    import("@/components/Dialog/FileList.vue")
);
  // Tab相关
  const activeTab = ref("scheduled");
// Tab相关
const activeTab = ref("scheduled");
  // 计划弹窗控制器
  const planModalRef = ref();
  // 保养弹窗控制器
  const maintainModalRef = ref();
  // 定时任务弹窗控制器
  const formDiaRef = ref();
  // 附件弹窗
  const fileListDialogRef = ref(null);
  const fileDialogVisible = ref(false);
  const currentMaintenanceTaskId = ref(null);
// 计划弹窗控制器
const planModalRef = ref();
// 保养弹窗控制器
const maintainModalRef = ref();
// 定时任务弹窗控制器
const formDiaRef = ref();
// 附件弹窗
const fileListDialogRef = ref(null);
const fileDialogVisible = ref(false);
const currentMaintenanceTaskId = ref(null);
  // 保养记录tab(原设备保养页面)相关变量
  const filters = reactive({
    deviceName: "",
    maintenancePlanTime: "",
    maintenanceActuallyTime: "",
    maintenanceActuallyName: "",
  });
// 保养记录tab(原设备保养页面)相关变量
const filters = reactive({
    deviceName: "",
    maintenancePlanTime: "",
    maintenanceActuallyTime: "",
    maintenanceActuallyName: "",
});
  const dataList = ref([]);
  const pagination = ref({
    currentPage: 1,
    pageSize: 10,
    total: 0,
  });
  const multipleList = ref([]);
const dataList = ref([]);
const pagination = ref({
    currentPage: 1,
    pageSize: 100,
    total: 0,
});
const multipleList = ref([]);
const tableLoading = ref(false);
const expandedRowKeys = ref([]);
  // 保养任务tab相关变量
  const scheduledFilters = reactive({
    taskName: "",
    status: "",
  });
const groupByDate = (list) => {
    const map = {};
    list.forEach(item => {
        const date = item.dateStr || (item.maintenancePlanTime ? dayjs(item.maintenancePlanTime).format('YYYY-MM-DD') : (item.createTime ? dayjs(item.createTime).format('YYYY-MM-DD') : '未知日期'));
        if (!map[date]) map[date] = [];
        map[date].push(item);
    });
    return Object.entries(map).map(([date, children]) => ({ date, children, count: children.length }));
};
  const scheduledDataList = ref([]);
  const scheduledPagination = reactive({
    currentPage: 1,
    pageSize: 10,
    total: 0,
  });
  const scheduledMultipleList = ref([]);
const groupedDataList = computed(() => groupByDate(dataList.value));
  // 保养任务表格列配置
  const scheduledColumns = ref([
    { prop: "taskName", label: "设备名称" },
    {
      label: "规格型号",
      prop: "deviceModel",
    },
    {
      label: "保养项目",
      prop: "machineryCategory",
      minWidth: 120,
      formatData: cell => cell || "--",
    },
    {
      prop: "frequencyType",
      label: "频次",
      minWidth: 150,
      // PIMTable 使用的是 formatData,而不是 Element-Plus 的 formatter
      formatData: cell =>
        ({
          DAILY: "每日",
          WEEKLY: "每周",
          MONTHLY: "每月",
          QUARTERLY: "季度",
        }[cell] || ""),
    },
    {
      prop: "frequencyDetail",
      label: "开始日期与时间",
      minWidth: 150,
      // 同样改用 formatData,PIMTable 内部会把单元格值传进来
      formatData: cell => {
        if (typeof cell !== "string") return "";
        let val = cell;
        const replacements = {
          MON: "周一",
          TUE: "周二",
          WED: "周三",
          THU: "周四",
          FRI: "周五",
          SAT: "周六",
          SUN: "周日",
        };
        // 使用正则一次性替换所有匹配项
        return val.replace(
          /MON|TUE|WED|THU|FRI|SAT|SUN/g,
          match => replacements[match]
        );
      },
    },
    { prop: "maintenancePerson", label: "保养人", minWidth: 100 },
    { prop: "registrant", label: "登记人", minWidth: 100 },
    {
      prop: "registrationDate",
      label: "登记日期",
      minWidth: 100,
      formatData: cell =>
        cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : "-",
    },
    {
      fixed: "right",
      label: "操作",
      dataType: "slot",
      slot: "operation",
      align: "center",
      width: "200px",
    },
  ]);
// 单条保养完成
const handleSingleComplete = async (id) => {
    try {
        await ElMessageBox.confirm('确认将该保养记录标记为完成?', '提示', { type: 'warning' });
        const res = await completeMaintenance(id);
        if (res.code === 200) {
            ElMessage.success('保养完成');
            getTableData();
        } else {
            ElMessage.error(res.msg || '操作失败');
        }
    } catch { /* 取消 */ }
};
  // 保养记录表格列配置(原设备保养表格列)
  const columns = ref([
    {
      label: "设备名称",
      align: "center",
      prop: "deviceName",
    },
    {
      label: "规格型号",
      align: "center",
      prop: "deviceModel",
    },
    {
      label: "计划保养日期",
      align: "center",
      prop: "maintenancePlanTime",
      formatData: cell => {
        return cell == null ? "-" : dayjs(cell).format("YYYY-MM-DD");
      },
    },
    {
      label: "录入人",
      align: "center",
      prop: "createUserName",
    },
    {
      label: "保养项目",
      align: "center",
      prop: "machineryCategory",
      formatData: cell => cell || "--",
    },
    // {
    //   label: "录入日期",
    //   align: "center",
    //   prop: "createTime",
    //   formatData: (cell) => dayjs(cell).format("YYYY-MM-DD HH:mm:ss"),
    //   width: 200,
    // },
    {
      label: "实际保养人",
      align: "center",
      prop: "maintenanceActuallyName",
    },
    {
      label: "实际保养日期",
      align: "center",
      prop: "maintenanceActuallyTime",
      formatData: cell =>
        cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : "-",
    },
    {
      label: "保养结果",
      align: "center",
      prop: "maintenanceResult",
      dataType: "slot",
      slot: "maintenanceResultRef",
    },
    {
      label: "状态",
      align: "center",
      prop: "status",
      dataType: "slot",
      slot: "statusRef",
    },
    {
      fixed: "right",
      label: "操作",
      dataType: "slot",
      slot: "operation",
      align: "center",
      width: "350px",
    },
  ]);
// 一键保养完成
const handleBatchComplete = async (dateStr) => {
    try {
        await ElMessageBox.confirm(`确认将 ${dateStr} 的所有待保养记录标记为完成?`, '提示', { type: 'warning' });
        const res = await batchCompleteMaintenance(dateStr);
        if (res.code === 200) {
            ElMessage.success(res.msg || '批量保养完成成功');
            getTableData();
        } else {
            ElMessage.error(res.msg || '批量保养完成失败');
        }
    } catch { /* 取消 */ }
};
  // Tab切换处理
  const handleTabChange = tabName => {
    if (tabName === "record") {
      getTableData();
    } else if (tabName === "scheduled") {
      getScheduledTableData();
    }
  };
const onExpandChange = (row, expandedRows) => {
    expandedRowKeys.value = expandedRows.map(r => r.date);
};
  // 保养任务相关方法
  const getScheduledTableData = async () => {
    try {
      const params = {
        current: scheduledPagination.currentPage,
        size: scheduledPagination.pageSize,
        taskName: scheduledFilters.taskName || undefined,
        status: scheduledFilters.status || undefined,
      };
      const { code, data } = await deviceMaintenanceTaskList(params);
      if (code === 200) {
        scheduledDataList.value = data?.records || [];
        scheduledPagination.total = data?.total || 0;
      }
    } catch (error) {
      ElMessage.error("获取定时任务列表失败");
    }
  };
// 保养任务tab相关变量
const scheduledFilters = reactive({
    taskName: "",
    status: "",
});
  const resetScheduledFilters = () => {
    scheduledFilters.taskName = "";
    scheduledFilters.status = "";
    getScheduledTableData();
  };
const scheduledDataList = ref([]);
const scheduledPagination = reactive({
    currentPage: 1,
    pageSize: 10,
    total: 0,
});
const scheduledMultipleList = ref([]);
  const handleScheduledSelectionChange = selection => {
    scheduledMultipleList.value = selection;
  };
// 保养任务表格列配置
const scheduledColumns = ref([
    { prop: "taskName", label: "设备名称" },
    {
        label: "规格型号",
        prop: "deviceModel",
    },
    {
        label: "保养项目",
        prop: "machineryCategory",
        minWidth: 120,
        formatData: cell => cell || "--",
    },
    {
        prop: "frequencyType",
        label: "频次",
        minWidth: 150,
        // PIMTable 使用的是 formatData,而不是 Element-Plus 的 formatter
        formatData: cell =>
            ({
                DAILY: "每日",
                WEEKLY: "每周",
                MONTHLY: "每月",
                QUARTERLY: "季度",
            }[cell] || ""),
    },
    {
        prop: "frequencyDetail",
        label: "开始日期与时间",
        minWidth: 150,
        // 同样改用 formatData,PIMTable 内部会把单元格值传进来
        formatData: cell => {
            if (typeof cell !== "string") return "";
            let val = cell;
            const replacements = {
                MON: "周一",
                TUE: "周二",
                WED: "周三",
                THU: "周四",
                FRI: "周五",
                SAT: "周六",
                SUN: "周日",
            };
            // 使用正则一次性替换所有匹配项
            return val.replace(
                /MON|TUE|WED|THU|FRI|SAT|SUN/g,
                match => replacements[match]
            );
        },
    },
    { prop: "maintenancePerson", label: "保养人", minWidth: 100 },
    { prop: "registrant", label: "登记人", minWidth: 100 },
    {
        prop: "registrationDate",
        label: "登记日期",
        minWidth: 100,
        formatData: cell =>
            cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : "-",
    },
    {
        fixed: "right",
        label: "操作",
        dataType: "slot",
        slot: "operation",
        align: "center",
        width: "200px",
    },
]);
  const changeScheduledPage = page => {
    scheduledPagination.currentPage = page.page;
    scheduledPagination.pageSize = page.limit;
    getScheduledTableData();
  };
// 保养记录表格列配置(原设备保养表格列)
const columns = ref([
    {
        label: "设备名称",
        align: "center",
        prop: "deviceName",
    },
    {
        label: "规格型号",
        align: "center",
        prop: "deviceModel",
    },
    {
        label: "计划保养日期",
        align: "center",
        prop: "maintenancePlanTime",
        formatData: cell => {
            return cell == null ? "-" : dayjs(cell).format("YYYY-MM-DD");
        },
    },
    {
        label: "录入人",
        align: "center",
        prop: "createUserName",
    },
    {
        label: "保养项目",
        align: "center",
        prop: "machineryCategory",
        formatData: cell => cell || "--",
    },
    // {
    //   label: "录入日期",
    //   align: "center",
    //   prop: "createTime",
    //   formatData: (cell) => dayjs(cell).format("YYYY-MM-DD HH:mm:ss"),
    //   width: 200,
    // },
    {
        label: "实际保养人",
        align: "center",
        prop: "maintenanceActuallyName",
    },
    {
        label: "实际保养日期",
        align: "center",
        prop: "maintenanceActuallyTime",
        formatData: cell =>
            cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : "-",
    },
    {
        label: "保养结果",
        align: "center",
        prop: "maintenanceResult",
        dataType: "slot",
        slot: "maintenanceResultRef",
    },
    {
        label: "状态",
        align: "center",
        prop: "status",
        dataType: "slot",
        slot: "statusRef",
    },
    {
        fixed: "right",
        label: "操作",
        dataType: "slot",
        slot: "operation",
        align: "center",
        width: "350px",
    },
]);
  const addScheduledTask = () => {
    nextTick(() => {
      formDiaRef.value?.openDialog("add");
    });
  };
// Tab切换处理
const handleTabChange = tabName => {
    if (tabName === "record") {
        getTableData();
    } else if (tabName === "scheduled") {
        getScheduledTableData();
    }
};
  const editScheduledTask = row => {
    if (row) {
      nextTick(() => {
        formDiaRef.value?.openDialog("edit", row);
      });
    }
  };
// 保养任务相关方法
const getScheduledTableData = async () => {
    try {
        const params = {
            current: scheduledPagination.currentPage,
            size: scheduledPagination.pageSize,
            taskName: scheduledFilters.taskName || undefined,
            status: scheduledFilters.status || undefined,
        };
        const { code, data } = await deviceMaintenanceTaskList(params);
        if (code === 200) {
            scheduledDataList.value = data?.records || [];
            scheduledPagination.total = data?.total || 0;
        }
    } catch (error) {
        ElMessage.error("获取定时任务列表失败");
    }
};
  const delScheduledTaskByIds = async ids => {
    try {
      await ElMessageBox.confirm("确定删除选中的定时任务吗?", "提示", {
        type: "warning",
      });
      const payload = Array.isArray(ids) ? ids : [ids];
      await deviceMaintenanceTaskDel(payload);
      ElMessage.success("删除定时任务成功");
      getScheduledTableData();
    } catch (error) {
      // 用户取消删除
    }
  };
const resetScheduledFilters = () => {
    scheduledFilters.taskName = "";
    scheduledFilters.status = "";
    getScheduledTableData();
};
  const handleScheduledOut = () => {
    ElMessage.info("导出定时任务功能待实现");
  };
const handleScheduledSelectionChange = selection => {
    scheduledMultipleList.value = selection;
};
  // 保养记录相关方法(原设备保养页面方法)
  const getTableData = async () => {
    try {
      const params = {
        current: pagination.value.currentPage,
        size: pagination.value.pageSize,
        deviceName: filters.deviceName || undefined,
        maintenancePlanTime: filters.maintenancePlanTime
          ? dayjs(filters.maintenancePlanTime).format("YYYY-MM-DD")
          : undefined,
        maintenanceActuallyTime: filters.maintenanceActuallyTime
          ? dayjs(filters.maintenanceActuallyTime).format("YYYY-MM-DD")
          : undefined,
        maintenanceActuallyName: filters.maintenanceActuallyName || undefined,
      };
const changeScheduledPage = page => {
    scheduledPagination.currentPage = page.page;
    scheduledPagination.pageSize = page.limit;
    getScheduledTableData();
};
      const { code, data } = await getUpkeepPage(params);
      if (code === 200) {
        dataList.value = data.records;
        pagination.value.total = data.total;
      }
    } catch (error) {
      console.log(error);
    }
  };
const addScheduledTask = () => {
    nextTick(() => {
        formDiaRef.value?.openDialog("add");
    });
};
  const resetFilters = () => {
    filters.deviceName = "";
    filters.maintenancePlanTime = "";
    filters.maintenanceActuallyTime = "";
    filters.maintenanceActuallyName = "";
    getTableData();
  };
const editScheduledTask = row => {
    if (row) {
        nextTick(() => {
            formDiaRef.value?.openDialog("edit", row);
        });
    }
};
  const handleSelectionChange = selection => {
    multipleList.value = selection;
  };
const delScheduledTaskByIds = async ids => {
    try {
        await ElMessageBox.confirm("确定删除选中的定时任务吗?", "提示", {
            type: "warning",
        });
        const payload = Array.isArray(ids) ? ids : [ids];
        await deviceMaintenanceTaskDel(payload);
        ElMessage.success("删除定时任务成功");
        getScheduledTableData();
    } catch (error) {
        // 用户取消删除
    }
};
  // 检查选中的记录中是否有完结状态的
  const hasFinishedStatus = computed(() => {
    return multipleList.value.some(item => item.status === 1);
  });
const handleScheduledOut = () => {
    ElMessage.info("导出定时任务功能待实现");
};
  const changePage = page => {
    pagination.value.currentPage = page.page;
    pagination.value.pageSize = page.limit;
    getTableData();
  };
// 保养记录相关方法(原设备保养页面方法)
const getTableData = async () => {
    tableLoading.value = true;
    try {
        const params = {
            current: pagination.value.currentPage,
            size: pagination.value.pageSize,
            deviceName: filters.deviceName || undefined,
            maintenancePlanTime: filters.maintenancePlanTime
                ? dayjs(filters.maintenancePlanTime).format("YYYY-MM-DD")
                : undefined,
            maintenanceActuallyTime: filters.maintenanceActuallyTime
                ? dayjs(filters.maintenanceActuallyTime).format("YYYY-MM-DD")
                : undefined,
            maintenanceActuallyName: filters.maintenanceActuallyName || undefined,
        };
        const { code, data } = await getUpkeepPage(params);
        if (code === 200) {
            dataList.value = data.records;
            pagination.value.total = data.total;
        }
    } catch (error) {
        console.log(error);
    } finally {
        tableLoading.value = false;
    }
};
  const addMaintain = row => {
    maintainModalRef.value.open(row.id, row);
  };
const resetFilters = () => {
    filters.deviceName = "";
    filters.maintenancePlanTime = "";
    filters.maintenanceActuallyTime = "";
    filters.maintenanceActuallyName = "";
    getTableData();
};
  const addPlan = () => {
    planModalRef.value.openModal();
  };
const handleSelectionChange = selection => {
    // 从选中的分组行中收集所有子记录ID
    const ids = [];
    selection.forEach(group => {
        if (group.children) {
            group.children.forEach(child => ids.push(child.id));
        }
    });
    multipleList.value = ids;
};
  const editPlan = id => {
    planModalRef.value.openEdit(id);
  };
const changePage = page => {
    pagination.value.currentPage = page.page;
    pagination.value.pageSize = page.limit;
    getTableData();
};
  const delRepairByIds = async ids => {
    // 检查是否有完结状态的记录
    const hasFinished = multipleList.value.some(item => item.status === 1);
    if (hasFinished) {
      ElMessage.warning("不能删除状态为完结的记录");
      return;
    }
const addMaintain = row => {
    maintainModalRef.value.open(row.id, row);
};
    try {
      await ElMessageBox.confirm("确认删除保养数据, 此操作不可逆?", "警告", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      });
const addPlan = () => {
    planModalRef.value.openModal();
};
      const { code } = await delUpkeep(ids);
      if (code === 200) {
        ElMessage.success("删除成功");
        getTableData();
      }
    } catch (error) {
      // 用户取消删除
    }
  };
const editPlan = id => {
    planModalRef.value.openEdit(id);
};
  const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        proxy.download("/device/maintenance/export", {}, "设备保养.xlsx");
      })
      .catch(() => {
        ElMessage.info("已取消");
      });
  };
const delRepairByIds = async ids => {
    const idList = Array.isArray(ids) ? ids : [ids];
    if (idList.length === 0) return;
    try {
        await ElMessageBox.confirm("确认删除保养数据, 此操作不可逆?", "警告", {
            confirmButtonText: "确定",
            cancelButtonText: "取消",
            type: "warning",
        });
        const { code } = await delUpkeep(idList);
        if (code === 200) {
            ElMessage.success("删除成功");
            getTableData();
        }
    } catch (error) {
        // 用户取消删除
    }
};
  const handleDateChange = (date, type) => {
    if (type === 1) {
      filters.maintenanceActuallyTime = date
        ? dayjs(date).format("YYYY-MM-DD")
        : "";
    } else {
      filters.maintenancePlanTime = date ? dayjs(date).format("YYYY-MM-DD") : "";
    }
    getTableData();
  };
const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
    })
        .then(() => {
            proxy.download("/device/maintenance/export", {}, "设备保养.xlsx");
        })
        .catch(() => {
            ElMessage.info("已取消");
        });
};
  // 打开附件弹窗
  const openFileDialog = async row => {
    currentMaintenanceTaskId.value = row.id;
    fileDialogVisible.value = true;
  };
const handleDateChange = (date, type) => {
    if (type === 1) {
        filters.maintenanceActuallyTime = date
            ? dayjs(date).format("YYYY-MM-DD")
            : "";
    } else {
        filters.maintenancePlanTime = date ? dayjs(date).format("YYYY-MM-DD") : "";
    }
    getTableData();
};
  onMounted(() => {
    // 根据默认激活的 Tab 调用对应的查询接口
    if (activeTab.value === "scheduled") {
      getScheduledTableData();
    } else {
      getTableData();
    }
  });
// 打开附件弹窗
const openFileDialog = async row => {
    currentMaintenanceTaskId.value = row.id;
    fileDialogVisible.value = true;
};
onMounted(() => {
    // 根据默认激活的 Tab 调用对应的查询接口
    if (activeTab.value === "scheduled") {
        getScheduledTableData();
    } else {
        getTableData();
    }
});
</script>
<style lang="scss" scoped>
  .table_list {
    margin-top: unset;
  }
  .actions {
    display: flex;
    justify-content: space-between;
    margin-bottom: 10px;
  }
.table_list {
    margin-top: unset;
}
.actions {
    display: flex;
    justify-content: space-between;
    margin-bottom: 10px;
}
</style>
src/views/reportAnalysis/PSIDataAnalysis/components/center-bottom.vue
@@ -1,25 +1,25 @@
<template>
  <div>
    <PanelHeader title="出入库趋势" />
    <div class="main-panel panel-item-customers">
      <div class="filters-row">
        <ProductTypeSwitch v-model="productType" @change="handleFilterChange" />
      </div>
      <Echarts
        ref="chart"
        :chartStyle="chartStyle"
        :grid="grid"
        :legend="lineLegend"
        :series="lineSeries"
        :tooltip="tooltip"
        :xAxis="xAxis1"
        :yAxis="yAxis1"
        :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
        style="height: 260px"
      />
    </div>
  </div>
    <div>
        <PanelHeader title="出入库趋势" />
        <div class="main-panel panel-item-customers">
            <div class="filters-row">
                <ProductTypeSwitch v-model="productType" @change="handleFilterChange" />
            </div>
            <Echarts
                ref="chart"
                :chartStyle="chartStyle"
                :grid="grid"
                :legend="lineLegend"
                :series="lineSeries"
                :tooltip="tooltip"
                :xAxis="xAxis1"
                :yAxis="yAxis1"
                :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
                style="height: 260px"
            />
        </div>
    </div>
</template>
<script setup>
@@ -35,154 +35,157 @@
const chartStyle = { width: '100%', height: '130%' }
const grid = {
  left: '3%',
  right: '4%',
  bottom: '3%',
  top: '16%',
  containLabel: true,
    left: '3%',
    right: '4%',
    bottom: '3%',
    top: '16%',
    containLabel: true,
}
const lineLegend = {
  show: true,
  top: '2%',
  left: 'center',
  itemGap: 24,
  itemWidth: 12,
  itemHeight: 12,
  textStyle: { color: '#B8C8E0', fontSize: 14 },
  data: [
    { name: '出库', itemStyle: { color: 'rgba(11, 137, 254, 1)' } },
    { name: '入库', itemStyle: { color: 'rgba(11, 249, 254, 1)' } },
  ],
    show: true,
    top: '2%',
    left: 'center',
    itemGap: 24,
    itemWidth: 12,
    itemHeight: 12,
    textStyle: { color: '#B8C8E0', fontSize: 14 },
    data: [
        { name: '出库', itemStyle: { color: 'rgba(11, 137, 254, 1)' } },
        { name: '入库', itemStyle: { color: 'rgba(11, 249, 254, 1)' } },
    ],
}
const xAxis1 = ref([
  {
    type: 'category',
    data: [],
    axisTick: { show: false },
    axisLine: { show: false, lineStyle: { color: 'rgba(184, 200, 224, 0.3)' } },
    axisLabel: { color: '#B8C8E0', fontSize: 12 },
    splitLine: { show: false, lineStyle: { type: 'dashed', color: 'rgba(184, 200, 224, 0.2)' } },
  },
    {
        type: 'category',
        data: [],
        axisTick: { show: false },
        axisLine: { show: false, lineStyle: { color: 'rgba(184, 200, 224, 0.3)' } },
        axisLabel: { color: '#B8C8E0', fontSize: 12 },
        splitLine: { show: false, lineStyle: { type: 'dashed', color: 'rgba(184, 200, 224, 0.2)' } },
    },
])
const yAxis1 = [
  {
    type: 'value',
    name: '单位: 件',
    nameTextStyle: { color: '#B8C8E0', fontSize: 12, padding: [0, 0, 0, 0] },
    axisLine: { show: false },
    axisTick: { show: false },
    axisLabel: { color: '#B8C8E0', fontSize: 12 },
    splitLine: { lineStyle: { color: '#B8C8E0' } },
  },
    {
        type: 'value',
        name: '单位: 件',
        nameTextStyle: { color: '#B8C8E0', fontSize: 12, padding: [0, 0, 0, 0] },
        axisLine: { show: false },
        axisTick: { show: false },
        axisLabel: { color: '#B8C8E0', fontSize: 12 },
        splitLine: { lineStyle: { color: '#B8C8E0' } },
    },
]
const lineSeries = ref([
  {
    name: '出库',
    type: 'line',
    smooth: false,
    showSymbol: true,
    symbol: 'circle',
    symbolSize: 8,
    lineStyle: { color: 'rgba(11, 137, 254, 1)', width: 2 },
    itemStyle: { color: 'rgba(11, 137, 254, 1)', borderWidth: 0 },
    areaStyle: {
      color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
        { offset: 0, color: 'rgba(11, 137, 254, 0.40)' },
        { offset: 1, color: 'rgba(11, 137, 254, 0.05)' },
      ]),
    },
    data: [],
    emphasis: { focus: 'series' },
  },
  {
    name: '入库',
    type: 'line',
    smooth: false,
    showSymbol: true,
    symbol: 'circle',
    symbolSize: 8,
    lineStyle: { color: 'rgba(11, 249, 254, 1)', width: 2 },
    itemStyle: { color: 'rgba(11, 249, 254, 1)', borderWidth: 0 },
    areaStyle: {
      color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
        { offset: 0, color: 'rgba(11, 249, 254, 0.5)' },
        { offset: 1, color: 'rgba(11, 249, 254, 0.05)' },
      ]),
    },
    data: [],
    emphasis: { focus: 'series' },
  },
    {
        name: '出库',
        type: 'line',
        smooth: false,
        showSymbol: true,
        symbol: 'circle',
        symbolSize: 8,
        lineStyle: { color: 'rgba(11, 137, 254, 1)', width: 2 },
        itemStyle: { color: 'rgba(11, 137, 254, 1)', borderWidth: 0 },
        areaStyle: {
            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                { offset: 0, color: 'rgba(11, 137, 254, 0.40)' },
                { offset: 1, color: 'rgba(11, 137, 254, 0.05)' },
            ]),
        },
        data: [],
        emphasis: { focus: 'series' },
    },
    {
        name: '入库',
        type: 'line',
        smooth: false,
        showSymbol: true,
        symbol: 'circle',
        symbolSize: 8,
        lineStyle: { color: 'rgba(11, 249, 254, 1)', width: 2 },
        itemStyle: { color: 'rgba(11, 249, 254, 1)', borderWidth: 0 },
        areaStyle: {
            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                { offset: 0, color: 'rgba(11, 249, 254, 0.5)' },
                { offset: 1, color: 'rgba(11, 249, 254, 0.05)' },
            ]),
        },
        data: [],
        emphasis: { focus: 'series' },
    },
])
const tooltip = {
  trigger: 'axis',
  axisPointer: { type: 'line' },
  borderWidth: 1,
  textStyle: { fontSize: 12 },
  formatter(params) {
    let result = params[0].axisValue + '<br/>'
    params.forEach((item) => {
      result += `${item.marker} ${item.seriesName}: ${item.value} 件<br/>`
    })
    return result
  },
    trigger: 'axis',
    axisPointer: { type: 'line' },
    borderWidth: 1,
    textStyle: { fontSize: 12 },
    formatter(params) {
        let result = params[0].axisValue + '<br/>'
        params.forEach((item) => {
            result += `${item.marker} ${item.seriesName}: ${item.value} 件<br/>`
        })
        return result
    },
}
const dateRange = inject('psiDateRange', null)
const fetchData = () => {
  productInOutAnalysis({ type: productType.value })
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        const list = res.data
        xAxis1.value[0].data = list.map((d) => d.date)
        lineSeries.value[0].data = list.map((d) => Number(d.outCount) || 0)
        lineSeries.value[1].data = list.map((d) => Number(d.inCount) || 0)
      }
    })
    .catch((err) => {
      console.error('获取产品出入库分析失败:', err)
    })
    const params = { type: productType.value, ...(dateRange?.value || {}) }
    productInOutAnalysis(params)
        .then((res) => {
            if (res.code === 200 && Array.isArray(res.data)) {
                const list = res.data
                xAxis1.value[0].data = list.map((d) => d.date)
                lineSeries.value[0].data = list.map((d) => Number(d.outCount) || 0)
                lineSeries.value[1].data = list.map((d) => Number(d.inCount) || 0)
            }
        })
        .catch((err) => {
            console.error('获取产品出入库分析失败:', err)
        })
}
const handleFilterChange = () => {
  fetchData()
    fetchData()
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
    watch(dataDashboardRefreshTick, () => {
        fetchData()
    })
}
onMounted(() => {
  fetchData()
    fetchData()
})
</script>
<style scoped>
.main-panel {
  display: flex;
  flex-direction: column;
  gap: 20px;
    display: flex;
    flex-direction: column;
    gap: 20px;
}
.filters-row {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: 12px;
    margin-bottom: 10px;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 428px;
    border: 1px solid #1a58b0;
    padding: 18px;
    width: 100%;
    height: 428px;
}
</style>
src/views/reportAnalysis/PSIDataAnalysis/components/center-center.vue
@@ -1,29 +1,29 @@
<template>
  <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>
      <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>
    <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>
            <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>
@@ -31,106 +31,108 @@
import Echarts from '@/components/Echarts/echarts.vue'
import { productTurnoverDays } from '@/api/viewIndex.js'
const dateRange = inject('psiDateRange', null)
const chartStyle = { width: '100%', height: '100%' }
const grid = { left: '3%', right: '4%', bottom: '3%', top: '4%', containLabel: true }
const barLegend = { show: false, textStyle: { color: '#B8C8E0' }, data: ['周转天数'] }
const barSeries1 = ref([
  {
    name: '周转天数',
    type: 'bar',
    barGap: 0,
    barWidth: 30,
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
        type: 'linear',
        x: 0, y: 1, x2: 0, y2: 0,
        colorStops: [
          { offset: 0, color: 'rgba(0,164,237,0)' },
          { offset: 1, color: '#4EE4FF' },
        ],
      },
    },
    data: [],
  },
    {
        name: '周转天数',
        type: 'bar',
        barGap: 0,
        barWidth: 30,
        emphasis: { focus: 'series' },
        itemStyle: {
            color: {
                type: 'linear',
                x: 0, y: 1, x2: 0, y2: 0,
                colorStops: [
                    { offset: 0, color: 'rgba(0,164,237,0)' },
                    { offset: 1, color: '#4EE4FF' },
                ],
            },
        },
        data: [],
    },
])
const tooltip = {
  trigger: 'axis',
  axisPointer: { type: 'shadow' },
  formatter(params) {
    let result = params[0].axisValueLabel + '<br/>'
    params.forEach((item) => {
      result += `<div>${item.marker} ${item.seriesName}: ${item.value} 天</div>`
    })
    return result
  },
    trigger: 'axis',
    axisPointer: { type: 'shadow' },
    formatter(params) {
        let result = params[0].axisValueLabel + '<br/>'
        params.forEach((item) => {
            result += `<div>${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 fetchData = () => {
  productTurnoverDays()
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        const list = res.data
        xAxis1.value[0].data = list.map((d) => d.name)
        barSeries1.value[0].data = list.map((d) => Number(d.value) || 0)
      }
    })
    .catch((err) => {
      console.error('获取产品周转天数失败:', err)
    })
    productTurnoverDays(dateRange?.value || undefined)
        .then((res) => {
            if (res.code === 200 && Array.isArray(res.data)) {
                const list = res.data
                xAxis1.value[0].data = list.map((d) => d.name)
                barSeries1.value[0].data = list.map((d) => Number(d.value) || 0)
            }
        })
        .catch((err) => {
            console.error('获取产品周转天数失败:', err)
        })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
    watch(dataDashboardRefreshTick, () => {
        fetchData()
    })
}
onMounted(() => {
  fetchData()
    fetchData()
})
</script>
<style scoped>
.equipment-stats {
  border: 1px solid #1a58b0;
  padding: 0 18px 18px;
  display: flex;
  flex-direction: column;
  gap: 16px;
    border: 1px solid #1a58b0;
    padding: 0 18px 18px;
    display: flex;
    flex-direction: column;
    gap: 16px;
}
.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;
    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;
    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;
    width: 50px;
    height: 50px;
}
</style>
src/views/reportAnalysis/PSIDataAnalysis/components/center-top.vue
@@ -1,26 +1,26 @@
<template>
  <div>
    <!-- 顶部统计卡片 -->
    <div class="stats-cards">
      <div
        v-for="item in statItems"
        :key="item.name"
        class="stat-card"
      >
        <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        <div class="card-content">
          <span class="card-label">{{ item.name }}</span>
          <span class="card-value">{{ item.value }}</span>
          <div class="card-compare" :class="compareClass(Number(item.rate))">
            <span>同比</span>
            <span class="compare-value">{{ formatPercent(item.rate) }}</span>
            <span class="compare-icon">{{ Number(item.rate) >= 0 ? '↑' : '↓' }}</span>
          </div>
        </div>
      </div>
    </div>
  </div>
    <div>
        <!-- 顶部统计卡片 -->
        <div class="stats-cards">
            <div
                v-for="item in statItems"
                :key="item.name"
                class="stat-card"
            >
                <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
                <div class="card-content">
                    <span class="card-label">{{ item.name }}</span>
                    <span class="card-value">{{ item.value }}</span>
                    <div class="card-compare" :class="compareClass(Number(item.rate))">
                        <span>同比</span>
                        <span class="compare-value">{{ formatPercent(item.rate) }}</span>
                        <span class="compare-icon">{{ Number(item.rate) >= 0 ? '↑' : '↓' }}</span>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>
<script setup>
@@ -30,115 +30,118 @@
const statItems = ref([])
const formatPercent = (val) => {
  const num = Number(val) || 0
  return `${num.toFixed(2)}%`
    const num = Number(val) || 0
    return `${num.toFixed(2)}%`
}
const compareClass = (val) => (val >= 0 ? 'compare-up' : 'compare-down')
const dateRange = inject('psiDateRange', null)
const fetchData = () => {
  salesPurchaseStorageProductCount()
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        statItems.value = res.data.map((item) => ({
          name: item.name,
          value: item.value,
          rate: item.rate,
        }))
      }
    })
    .catch((err) => {
      console.error('获取销售/采购/储存产品数失败:', err)
    })
    const params = dateRange?.value || {}
    salesPurchaseStorageProductCount(params.startDate, params.endDate)
        .then((res) => {
            if (res.code === 200 && Array.isArray(res.data)) {
                statItems.value = res.data.map((item) => ({
                    name: item.name === '销售产品数' ? '销售订单数' : item.name,
                    value: item.value,
                    rate: item.rate,
                }))
            }
        })
        .catch((err) => {
            console.error('获取销售/采购/储存产品数失败:', err)
        })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
    watch(dataDashboardRefreshTick, () => {
        fetchData()
    })
}
onMounted(() => {
  fetchData()
    fetchData()
})
</script>
<style scoped>
.stats-cards {
  display: flex;
  gap: 30px;
    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;
    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;
    width: 100px;
    height: 100px;
    margin: 20px 20px 0 10px;
}
.card-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
    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;
    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);
    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;
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 15px;
    color: #d0e7ff;
}
.card-compare > span:first-child {
  font-size: 13px;
  opacity: 0.8;
    font-size: 13px;
    opacity: 0.8;
}
.compare-value {
  font-weight: 600;
    font-weight: 600;
}
.compare-icon {
  font-size: 14px;
  position: relative;
  top: -1px; /* 轻微上移,让箭头与文字垂直居中对齐 */
    font-size: 14px;
    position: relative;
    top: -1px; /* 轻微上移,让箭头与文字垂直居中对齐 */
}
.compare-up .compare-value,
.compare-up .compare-icon {
  color: #00c853;
    color: #00c853;
}
.compare-down .compare-value,
.compare-down .compare-icon {
  color: #ff5252;
    color: #ff5252;
}
</style>
src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue
@@ -1,24 +1,24 @@
<template>
  <div>
    <PanelHeader title="采购品分布" />
    <div class="main-panel panel-item-customers">
      <CarouselCards :items="cardItems" :visible-count="3" />
      <div class="pie-chart-wrapper" ref="pieWrapperRef">
        <div class="pie-background" ref="pieBackgroundRef"></div>
        <Echarts
          ref="chart"
          :chartStyle="chartStyle"
          :legend="landLegend"
          :series="landSeries"
          :tooltip="landTooltip"
          :color="landColors"
          :options="pieOptions"
          style="height: 320px"
          class="land-chart"
        />
      </div>
    </div>
  </div>
    <div>
        <PanelHeader title="采购品分布" />
        <div class="main-panel panel-item-customers">
            <CarouselCards :items="cardItems" :visible-count="3" />
            <div class="pie-chart-wrapper" ref="pieWrapperRef">
                <div class="pie-background" ref="pieBackgroundRef"></div>
                <Echarts
                    ref="chart"
                    :chartStyle="chartStyle"
                    :legend="landLegend"
                    :series="landSeries"
                    :tooltip="landTooltip"
                    :color="landColors"
                    :options="pieOptions"
                    style="height: 320px"
                    class="land-chart"
                />
            </div>
        </div>
    </div>
</template>
<script setup>
@@ -28,6 +28,8 @@
import CarouselCards from './CarouselCards.vue'
import { rawMaterialPurchaseAmountRatio } from '@/api/viewIndex.js'
import { useChartBackground } from '@/hooks/useChartBackground.js'
const dateRange = inject('psiDateRange', null)
const pieWrapperRef = ref(null)
const pieBackgroundRef = ref(null)
@@ -39,11 +41,11 @@
 * @return {返回类型说明}
 */
function array2obj(array, key) {
  const resObj = {}
  for (let i = 0; i < array.length; i++) {
    resObj[array[i][key]] = array[i]
  }
  return resObj
    const resObj = {}
    for (let i = 0; i < array.length; i++) {
        resObj[array[i][key]] = array[i]
    }
    return resObj
}
// 数据列表(来自接口)
@@ -59,204 +61,204 @@
// 图例配置(右侧竖排)
const landLegend = computed(() => {
  const data = dataList.value.map((d, idx) => ({
    name: d.name,
    icon: 'circle',
    textStyle: {
      fontSize: 18,
      color: landColors[idx % landColors.length],
    },
  }))
  return {
    orient: 'vertical',
    top: 'center',
    left: '52%',
    itemGap: 30,
    data: data,
    formatter: function (name) {
      const item = landObjData.value[name]
      if (!item) return name
      return `{title|${name}}{value|${item.value}}{unit|元}{percent|${item.rate}}{unit|%}`
    },
    textStyle: {
      rich: {
        value: {
          color: '#43e8fc',
          fontSize: 14,
          fontWeight: 600,
          padding: [0, 0, 0, 10],
        },
        unit: {
          color: '#82baff',
          fontSize: 12,
          fontWeight: 600,
          padding: [0, 10, 0, 0],
        },
        percent: {
          color: '#43e8fc',
          fontSize: 14,
          fontWeight: 600,
          padding: [0, 0, 0, 0],
        },
        title: {
          fontSize: 12,
          padding: [0, 0, 0, 0],
        },
      },
    },
  }
    const data = dataList.value.map((d, idx) => ({
        name: d.name,
        icon: 'circle',
        textStyle: {
            fontSize: 18,
            color: landColors[idx % landColors.length],
        },
    }))
    return {
        orient: 'vertical',
        top: 'center',
        left: '52%',
        itemGap: 30,
        data: data,
        formatter: function (name) {
            const item = landObjData.value[name]
            if (!item) return name
            return `{title|${name}}{value|${item.value}}{unit|元}{percent|${item.rate}}{unit|%}`
        },
        textStyle: {
            rich: {
                value: {
                    color: '#43e8fc',
                    fontSize: 14,
                    fontWeight: 600,
                    padding: [0, 0, 0, 10],
                },
                unit: {
                    color: '#82baff',
                    fontSize: 12,
                    fontWeight: 600,
                    padding: [0, 10, 0, 0],
                },
                percent: {
                    color: '#43e8fc',
                    fontSize: 14,
                    fontWeight: 600,
                    padding: [0, 0, 0, 0],
                },
                title: {
                    fontSize: 12,
                    padding: [0, 0, 0, 0],
                },
            },
        },
    }
})
// 提示框
const landTooltip = {
  trigger: 'item',
  formatter: '{a} <br/>{b} : {c}元 ({d}%)',
    trigger: 'item',
    formatter: '{a} <br/>{b} : {c}元 ({d}%)',
}
// 双层环形饼图
const landSeries = ref([
  {
    name: '产品采购金额分析',
    type: 'pie',
    radius: ['40%', '60%'],
    center: ['25%', '50%'],
    itemStyle: {
      borderColor: '#0a1c3a',
      borderWidth: 2,
      color: function (params) {
        return landColors[params.dataIndex % landColors.length]
      },
    },
    label: {
      show: false
    },
    minAngle: 15,
    data: dataList.value,
    animationType: 'scale',
    animationEasing: 'elasticOut',
    animationDelay: function () {
      return Math.random() * 200
    },
  },
  {
    // 内圈
    type: 'pie',
    radius: ['40%', '45%'],
    center: ['25%', '50%'],
    silent: true,
    label: {
      show: false,
    },
    labelLine: {
      show: false,
    },
    itemStyle: {
      color: 'rgba(0, 127, 255, 0.25)',
    },
    data: [1],
  },
    {
        name: '产品采购金额分析',
        type: 'pie',
        radius: ['40%', '60%'],
        center: ['25%', '50%'],
        itemStyle: {
            borderColor: '#0a1c3a',
            borderWidth: 2,
            color: function (params) {
                return landColors[params.dataIndex % landColors.length]
            },
        },
        label: {
            show: false
        },
        minAngle: 15,
        data: dataList.value,
        animationType: 'scale',
        animationEasing: 'elasticOut',
        animationDelay: function () {
            return Math.random() * 200
        },
    },
    {
        // 内圈
        type: 'pie',
        radius: ['40%', '45%'],
        center: ['25%', '50%'],
        silent: true,
        label: {
            show: false,
        },
        labelLine: {
            show: false,
        },
        itemStyle: {
            color: 'rgba(0, 127, 255, 0.25)',
        },
        data: [1],
    },
])
const chartStyle = {
  width: '100%',
  height: '100%',
    width: '100%',
    height: '100%',
}
const pieOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: '#B8C8E0' },
    backgroundColor: 'transparent',
    textStyle: { color: '#B8C8E0' },
}
// 使用封装的背景位置调整方法
// 图表中心是 ['25%', '50%'],背景需要对齐到这个位置
const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({
  wrapperRef: pieWrapperRef,
  backgroundRef: pieBackgroundRef,
  left: '25%',       // 图表中心 X 是 25%
  top: '50%',        // 图表中心 Y 是 50%
  offsetX: '-51.5%', // X 轴偏移
  offsetY: '-50%',   // Y 轴偏移
  watchData: dataList // 监听数据变化,自动调整位置
    wrapperRef: pieWrapperRef,
    backgroundRef: pieBackgroundRef,
    left: '25%',       // 图表中心 X 是 25%
    top: '50%',        // 图表中心 Y 是 50%
    offsetX: '-51.5%', // X 轴偏移
    offsetY: '-50%',   // Y 轴偏移
    watchData: dataList // 监听数据变化,自动调整位置
})
const fetchData = () => {
  rawMaterialPurchaseAmountRatio()
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        const items = res.data
        cardItems.value = items.map((item) => ({
          label: item.name,
          value: item.value,
          unit: '元',
          rate: item.rate,
        }))
        dataList.value = items.map((it) => ({
          name: it.name,
          value: parseFloat(it.value) || 0,
          rate: it.rate,
          children: [],
        }))
        landSeries.value[0].data = dataList.value
      }
    })
    .catch((err) => {
      console.error('获取原材料采购金额占比失败:', err)
    })
    rawMaterialPurchaseAmountRatio(dateRange?.value || undefined)
        .then((res) => {
            if (res.code === 200 && Array.isArray(res.data)) {
                const items = res.data
                cardItems.value = items.map((item) => ({
                    label: item.name,
                    value: item.value,
                    unit: '元',
                    rate: item.rate,
                }))
                dataList.value = items.map((it) => ({
                    name: it.name,
                    value: parseFloat(it.value) || 0,
                    rate: it.rate,
                    children: [],
                }))
                landSeries.value[0].data = dataList.value
            }
        })
        .catch((err) => {
            console.error('获取原材料采购金额占比失败:', err)
        })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
    watch(dataDashboardRefreshTick, () => {
        fetchData()
    })
}
onMounted(() => {
  fetchData()
  initBackground()
    fetchData()
    initBackground()
})
onBeforeUnmount(() => {
  cleanupBackground()
    cleanupBackground()
})
</script>
<style scoped>
.main-panel {
  display: flex;
  flex-direction: column;
  gap: 20px;
    display: flex;
    flex-direction: column;
    gap: 20px;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
    border: 1px solid #1a58b0;
    padding: 18px;
    width: 100%;
    height: 449px;
}
.pie-chart-wrapper {
  position: relative;
  width: 100%;
  height: 320px;
  background: transparent;
    position: relative;
    width: 100%;
    height: 320px;
    background: transparent;
}
.pie-background {
  position: absolute;
  width: 310px;
  height: 310px;
  background-image: url('@/assets/BI/玫瑰图边框.png');
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  z-index: 1;
  pointer-events: none;
  /* 位置由 JS 动态设置,默认居中 */
  left: 25%;
  top: 50%;
  transform: translate(-51.5%, -50%);
    position: absolute;
    width: 310px;
    height: 310px;
    background-image: url('@/assets/BI/玫瑰图边框.png');
    background-size: contain;
    background-position: center;
    background-repeat: no-repeat;
    z-index: 1;
    pointer-events: none;
    /* 位置由 JS 动态设置,默认居中 */
    left: 25%;
    top: 50%;
    transform: translate(-51.5%, -50%);
}
</style>
src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue
@@ -1,29 +1,31 @@
<template>
  <div>
    <PanelHeader title="销售品分布" />
    <div class="main-panel panel-item-customers">
      <CarouselCards :items="cardItems" :visible-count="3" />
      <div class="pie-chart-wrapper" ref="pieWrapperRef">
        <div class="pie-background" ref="pieBackgroundRef"></div>
        <Echarts
          ref="echartsRef"
          :chartStyle="chartStyle"
          :legend="pieLegend"
          :series="pieSeries"
          :tooltip="pieTooltip"
          :color="pieColors"
          :options="pieOptions"
          style="height: 320px"
        />
      </div>
    </div>
  </div>
    <div>
        <PanelHeader title="销售品分布" />
        <div class="main-panel panel-item-customers">
            <CarouselCards :items="cardItems" :visible-count="3" />
            <div class="pie-chart-wrapper" ref="pieWrapperRef">
                <div class="pie-background" ref="pieBackgroundRef"></div>
                <Echarts
                    ref="echartsRef"
                    :chartStyle="chartStyle"
                    :legend="pieLegend"
                    :series="pieSeries"
                    :tooltip="pieTooltip"
                    :color="pieColors"
                    :options="pieOptions"
                    style="height: 320px"
                />
            </div>
        </div>
    </div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, computed, inject, watch } from 'vue'
import { productSalesAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
const dateRange = inject('psiDateRange', null)
import CarouselCards from './CarouselCards.vue'
import Echarts from '@/components/Echarts/echarts.vue'
import { useChartBackground } from '@/hooks/useChartBackground.js'
@@ -38,16 +40,16 @@
 * @return {返回类型说明}
 */
function array2obj(array, key) {
  const resObj = {}
  for (let i = 0; i < array.length; i++) {
    resObj[array[i][key]] = array[i]
  }
  return resObj
    const resObj = {}
    for (let i = 0; i < array.length; i++) {
        resObj[array[i][key]] = array[i]
    }
    return resObj
}
const chartStyle = {
  width: '100%',
  height: '100%',
    width: '100%',
    height: '100%',
}
const echartsRef = ref(null)
@@ -57,171 +59,171 @@
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: '52%',
    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: 14,
          fontWeight: 600,
          padding: [0, 0, 0, 10],
        },
        unit: {
          color: '#82baff',
          fontSize: 12,
          fontWeight: 600,
          padding: [0, 10, 0, 0],
        },
        percent: {
          color: '#43e8fc',
          fontSize: 14,
          fontWeight: 600,
          padding: [0, 0, 0, 0],
        },
        title: {
          fontSize: 12,
          padding: [0, 0, 0, 0],
        },
      },
    },
  }
    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: '52%',
        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: 14,
                    fontWeight: 600,
                    padding: [0, 0, 0, 10],
                },
                unit: {
                    color: '#82baff',
                    fontSize: 12,
                    fontWeight: 600,
                    padding: [0, 10, 0, 0],
                },
                percent: {
                    color: '#43e8fc',
                    fontSize: 14,
                    fontWeight: 600,
                    padding: [0, 0, 0, 0],
                },
                title: {
                    fontSize: 12,
                    padding: [0, 0, 0, 0],
                },
            },
        },
    }
})
const pieTooltip = {
  trigger: 'item',
  formatter: '{a} <br/>{b} : {c}元 ({d}%)',
    trigger: 'item',
    formatter: '{a} <br/>{b} : {c}元 ({d}%)',
}
const pieSeries = computed(() => [
  {
    name: '产品销售金额分析',
    type: 'pie',
    radius: '60%',
    center: ['25%', '50%'],
    itemStyle: {
      borderColor: '#0a1c3a',
      borderWidth: 2,
    },
    label: {
      show: false
    },
    minAngle: 15,
    data: pieDatas.value,
    animationType: 'scale',
    animationEasing: 'elasticOut',
    animationDelay: function () {
      return Math.random() * 200
    },
  },
    {
        name: '产品销售金额分析',
        type: 'pie',
        radius: '60%',
        center: ['25%', '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' },
    backgroundColor: 'transparent',
    textStyle: { color: '#B8C8E0' },
}
const cardItems = ref([])
// 使用封装的背景位置调整方法(与其他文件保持一致)
const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({
  wrapperRef: pieWrapperRef,
  backgroundRef: pieBackgroundRef,
  left: '25%',       // 图表中心 X 是 25%
  top: '50%',        // 图表中心 Y 是 50%
  offsetX: '-51.5%', // X 轴偏移
  offsetY: '-50%',   // Y 轴偏移
  watchData: pieDatas // 监听数据变化,自动调整位置
    wrapperRef: pieWrapperRef,
    backgroundRef: pieBackgroundRef,
    left: '25%',       // 图表中心 X 是 25%
    top: '50%',        // 图表中心 Y 是 50%
    offsetX: '-51.5%', // X 轴偏移
    offsetY: '-50%',   // Y 轴偏移
    watchData: pieDatas // 监听数据变化,自动调整位置
})
const fetchData = () => {
  productSalesAnalysis()
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        const items = res.data
        cardItems.value = items.map((item) => ({
          label: item.name,
          value: item.value,
          unit: '元',
          rate: item.rate,
        }))
        pieDatas.value = items.map((item) => ({
          name: item.name,
          value: parseFloat(item.value) || 0,
          rate: item.rate,
        }))
      }
    })
    .catch((err) => {
      console.error('获取产品销售金额分析失败:', err)
    })
    productSalesAnalysis(dateRange?.value || undefined)
        .then((res) => {
            if (res.code === 200 && Array.isArray(res.data)) {
                const items = res.data
                cardItems.value = items.map((item) => ({
                    label: item.name,
                    value: item.value,
                    unit: '元',
                    rate: item.rate,
                }))
                pieDatas.value = items.map((item) => ({
                    name: item.name,
                    value: parseFloat(item.value) || 0,
                    rate: item.rate,
                }))
            }
        })
        .catch((err) => {
            console.error('获取产品销售金额分析失败:', err)
        })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
    watch(dataDashboardRefreshTick, () => {
        fetchData()
    })
}
onMounted(() => {
  fetchData()
  initBackground()
    fetchData()
    initBackground()
})
onBeforeUnmount(() => {
  cleanupBackground()
    cleanupBackground()
})
</script>
<style scoped>
.main-panel {
  display: flex;
  flex-direction: column;
  gap: 20px;
    display: flex;
    flex-direction: column;
    gap: 20px;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
    border: 1px solid #1a58b0;
    padding: 18px;
    width: 100%;
    height: 449px;
}
.pie-chart-wrapper {
  position: relative;
  width: 100%;
  height: 320px;
    position: relative;
    width: 100%;
    height: 320px;
}
.pie-background {
  position: absolute;
  left: 25%;
  top: 50%;
  transform: translate(-51.5%, -50%);
  width: 310px;
  height: 310px;
  background-image: url('@/assets/BI/玫瑰图边框.png');
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  z-index: 1;
  pointer-events: none;
    position: absolute;
    left: 25%;
    top: 50%;
    transform: translate(-51.5%, -50%);
    width: 310px;
    height: 310px;
    background-image: url('@/assets/BI/玫瑰图边框.png');
    background-size: contain;
    background-position: center;
    background-repeat: no-repeat;
    z-index: 1;
    pointer-events: none;
}
</style>
src/views/reportAnalysis/PSIDataAnalysis/index.vue
@@ -1,50 +1,66 @@
<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">PSI 数据分析</div>
    </div>
    <!-- 主要内容区域 -->
    <div class="dashboard-content">
      <!-- 左侧区域 -->
      <div class="left-panel">
        <LeftTop />
        <LeftBottom />
      </div>
      <!-- 中间区域 -->
      <div class="center-panel">
        <CenterTop />
        <CenterCenter/>
        <CenterBottom />
      </div>
      <!-- 右侧区域 -->
      <div class="right-panel">
        <RightBottom />
        <RightTop />
      </div>
    </div>
    </div>
  </div>
    <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="header-left"></div>
                <div class="factory-name">PSI 数据分析</div>
                <div class="header-right">
                    <div class="date-picker-wrapper">
                        <el-date-picker
                            v-model="psiDateRange"
                            type="daterange"
                            range-separator="至"
                            start-placeholder="开始日期"
                            end-placeholder="结束日期"
                            value-format="YYYY-MM-DD"
                            :shortcuts="dateShortcuts"
                            @change="onDateRangeChange"
                        />
                    </div>
                </div>
            </div>
            <!-- 主要内容区域 -->
            <div class="dashboard-content">
                <!-- 左侧区域 -->
                <div class="left-panel">
                    <LeftTop />
                    <LeftBottom />
                </div>
                <!-- 中间区域 -->
                <div class="center-panel">
                    <CenterTop />
                    <CenterCenter/>
                    <CenterBottom />
                </div>
                <!-- 右侧区域 -->
                <div class="right-panel">
                    <RightBottom />
                    <RightTop />
                </div>
            </div>
        </div>
    </div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick, provide } from 'vue'
import { ref, onMounted, onBeforeUnmount, nextTick, provide, reactive } from 'vue'
import autofit from 'autofit.js'
import dayjs from 'dayjs'
import LeftBottom from './components/left-bottom.vue'
import CenterCenter from './components/center-center.vue'
import RightTop from '../dataDashboard/components/basic/right-top.vue'
@@ -66,6 +82,27 @@
// 用户store
const userStore = useUserStore()
// 日期范围筛选
const psiDateRange = ref([])
const psiDateRangeParams = ref({})
const dateShortcuts = [
    { text: '本月', value: () => { const start = dayjs().startOf('month').format('YYYY-MM-DD'); const end = dayjs().format('YYYY-MM-DD'); return [start, end] } },
    { text: '近7天', value: () => { const start = dayjs().subtract(6, 'day').format('YYYY-MM-DD'); const end = dayjs().format('YYYY-MM-DD'); return [start, end] } },
    { text: '近30天', value: () => { const start = dayjs().subtract(29, 'day').format('YYYY-MM-DD'); const end = dayjs().format('YYYY-MM-DD'); return [start, end] } },
    { text: '近90天', value: () => { const start = dayjs().subtract(89, 'day').format('YYYY-MM-DD'); const end = dayjs().format('YYYY-MM-DD'); return [start, end] } },
]
const onDateRangeChange = (val) => {
    if (val && val.length === 2) {
        psiDateRangeParams.value = { startDate: val[0], endDate: val[1] }
    } else {
        psiDateRangeParams.value = {}
    }
    dataDashboardRefreshTick.value++
}
provide('psiDateRange', psiDateRangeParams)
/** 与 dataDashboard 共用注入名,子组件(含复用的 right-top/right-bottom)每分钟刷新 */
const DASHBOARD_REFRESH_MS = 60 * 1000
const dataDashboardRefreshTick = ref(0)
@@ -74,232 +111,253 @@
// 计算缩放比例
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)
    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)
}
// 窗口大小变化处理
const handleResize = () => {
  // 延迟执行,确保DOM更新完成
  setTimeout(() => {
    calculateScale()
  }, 100)
    // 延迟执行,确保DOM更新完成
    setTimeout(() => {
        calculateScale()
    }, 100)
}
// 全屏功能实现 - 针对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()
        }
    }
}
// 监听全屏变化事件
const handleFullscreenChange = () => {
  const fullscreenElement = document.fullscreenElement ||
                           document.webkitFullscreenElement ||
                           document.msFullscreenElement
  isFullscreen.value = fullscreenElement && fullscreenElement.classList.contains('scale-container')
  // 全屏状态变化时,延迟重新计算缩放比例(确保DOM更新完成)
  setTimeout(() => {
    calculateScale()
  }, 200)
    const fullscreenElement = document.fullscreenElement ||
        document.webkitFullscreenElement ||
        document.msFullscreenElement
    isFullscreen.value = fullscreenElement && fullscreenElement.classList.contains('scale-container')
    // 全屏状态变化时,延迟重新计算缩放比例(确保DOM更新完成)
    setTimeout(() => {
        calculateScale()
    }, 200)
}
// 生命周期钩子
onMounted(() => {
  // 使用nextTick确保DOM完全渲染后再初始化
  nextTick(() => {
    // 计算初始缩放比例
    calculateScale()
  })
  window.addEventListener('resize', handleResize)
  window.addEventListener('fullscreenchange', handleFullscreenChange)
  window.addEventListener('webkitfullscreenchange', handleFullscreenChange)
  window.addEventListener('MSFullscreenChange', handleFullscreenChange)
  dashboardPollTimer = setInterval(() => {
    dataDashboardRefreshTick.value++
  }, DASHBOARD_REFRESH_MS)
    // 使用nextTick确保DOM完全渲染后再初始化
    nextTick(() => {
        // 计算初始缩放比例
        calculateScale()
    })
    window.addEventListener('resize', handleResize)
    window.addEventListener('fullscreenchange', handleFullscreenChange)
    window.addEventListener('webkitfullscreenchange', handleFullscreenChange)
    window.addEventListener('MSFullscreenChange', handleFullscreenChange)
    dashboardPollTimer = setInterval(() => {
        dataDashboardRefreshTick.value++
    }, DASHBOARD_REFRESH_MS)
})
onBeforeUnmount(() => {
  if (dashboardPollTimer) {
    clearInterval(dashboardPollTimer)
    dashboardPollTimer = 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
  }
  // 关闭autofit
  autofit.off()
    if (dashboardPollTimer) {
        clearInterval(dashboardPollTimer)
        dashboardPollTimer = 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
    }
    // 关闭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;
    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: space-between;
}
.header-left {
    width: 300px;
    padding-left: 20px;
    display: flex;
    align-items: center;
}
.header-right {
    width: 300px;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    padding-right: 20px;
}
.date-picker-wrapper {
    /* 日期选择器在大屏上的样式 */
    --el-fill-color-blank: rgba(0, 20, 60, 0.8);
    --el-border-color: rgba(0, 212, 255, 0.3);
    --el-text-color-regular: #00d4ff;
    --el-color-primary: #00d4ff;
}
.factory-name {
font-weight: 600;
font-size: 52px;
color: #FFFFFF;
top: 16px;
position: absolute;
    font-weight: 600;
    font-size: 52px;
    color: #FFFFFF;
}
.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;
    flex: 1.5;
    display: flex;
    flex-direction: column;
    gap: 20px;
}
</style>
src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue
@@ -1,24 +1,24 @@
<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>
    <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>
@@ -28,31 +28,33 @@
import DateTypeSwitch from '../DateTypeSwitch.vue'
import { customerContributionRanking } from '@/api/viewIndex.js'
const dateRange = inject('psiDateRange', null)
const chartStyle = {
  width: '100%',
  height: '100%',
    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'
    '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',
    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 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))
@@ -60,277 +62,277 @@
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],
    }
  })
    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 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>${item.marker} ${item.seriesName}: ${item.value}元</div>`
    })
    return result
  },
    trigger: 'axis',
    textStyle: { fontSize: '100%' },
    formatter: function (params) {
        let result = params[0].axisValueLabel + '<br/>'
        params.forEach((item) => {
            result += `<div>${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 },
  },
    {
        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 },
  },
    {
        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,
  },
    {
        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 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 },
  ]
    // 模拟假数据(金额贡献排名)
    // 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)
    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)
    })
    customerContributionRanking({ type: dateType.value, ...(dateRange?.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()
    fetchCustomerRanking()
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchCustomerRanking()
  })
    watch(dataDashboardRefreshTick, () => {
        fetchCustomerRanking()
    })
}
onMounted(() => {
  fetchCustomerRanking()
    fetchCustomerRanking()
})
</script>
<style scoped>
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
    border: 1px solid #1a58b0;
    padding: 18px;
    width: 100%;
    height: 449px;
}
.switch-container {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 16px;
    display: flex;
    justify-content: flex-end;
    margin-bottom: 16px;
}
</style>
src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue
@@ -1,23 +1,23 @@
<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>
    <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>
@@ -27,23 +27,25 @@
import DateTypeSwitch from '../DateTypeSwitch.vue'
import { supplierPurchaseRanking } from '@/api/viewIndex.js'
const dateRange = inject('psiDateRange', null)
const chartStyle = {
  width: '100%',
  height: '100%',
    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'
    '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',
    0: '#ff5676',
    1: '#ffd83e',
    2: '#fbff94',
    3: '#7daeff',
}
// 原始数据
@@ -51,315 +53,315 @@
// 排序后的数据
const dataArray = computed(() => {
  return [...dataArr.value].sort((a, b) => a.NUM - b.NUM)
    return [...dataArr.value].sort((a, b) => a.NUM - b.NUM)
})
// 计算总数
const total = computed(() => {
  return dataArray.value.reduce((sum, v) => sum + Number(v.NUM), 0)
    return dataArray.value.reduce((sum, v) => sum + Number(v.NUM), 0)
})
// x轴数据(名称)
const xdataName = computed(() => {
  return dataArray.value.map((v) => v.NAME)
    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],
    }
  })
    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)
    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>${params[0].marker}${params[0].value}元</div>`
      return result
    },
  }
    return {
        trigger: 'axis',
        textStyle: { fontSize: '100%' },
        formatter: function (params) {
            let result = params[0].axisValueLabel + '<br/>'
            result += `<div>${params[0].marker}${params[0].value}元</div>`
            return result
        },
    }
})
// grid
const grid = computed(() => {
  return { top: 0, left: '20%', right: '10%', bottom: 0 }
    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 },
    },
  ]
    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 },
    },
  ]
    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,
    },
  ]
    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 },
      ]
    })
    supplierPurchaseRanking({ type: radio1.value, ...(dateRange?.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()
    fetchSupplierRanking()
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchSupplierRanking()
  })
    watch(dataDashboardRefreshTick, () => {
        fetchSupplierRanking()
    })
}
onMounted(() => {
  fetchSupplierRanking()
    fetchSupplierRanking()
})
</script>
<style scoped>
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
    border: 1px solid #1a58b0;
    padding: 18px;
    width: 100%;
    height: 449px;
}
.switch-container {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 16px;
    display: flex;
    justify-content: flex-end;
    margin-bottom: 16px;
}
.section-title {
  font-weight: 500;
  font-size: 16px;
  color: #d9ecff;
    font-weight: 500;
    font-size: 16px;
    color: #d9ecff;
}
</style>
src/views/reportAnalysis/taxComparison/index.vue
@@ -1,118 +1,411 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="日期">
        <el-date-picker
          style="width: 240px"
          v-model="filters.month"
          value-format="YYYY-MM"
          format="YYYY-MM"
          type="month"
          placeholder="选择月份"
          clearable
          @change="getTableData"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getTableData"> 搜索 </el-button>
        <el-button @click="resetFilters"> 重置 </el-button>
        <el-button @click="handleOut"> 导出 </el-button>
      </el-form-item>
    </el-form>
    <div class="table_list">
      <PIMTable
        rowKey="id"
        :column="columns"
        :tableData="dataList"
        :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @pagination="changePage"
      />
    </div>
  </div>
    <div class="app-container vat-page">
        <el-row :gutter="20" class="vat-row">
            <!-- 左侧:增值税明细列表 -->
            <el-col :span="15" class="vat-col">
                <el-card shadow="never" class="vat-card">
                    <template #header>
                        <div class="card-header">
                            <span class="card-title">增值税明细</span>
                            <el-select v-model="queryMonth" placeholder="选择月份" clearable style="width: 150px;" @change="handleMonthChange">
                                <el-option v-for="m in monthOptions" :key="m" :label="m" :value="m" />
                            </el-select>
                        </div>
                    </template>
                    <template v-if="!loading && vatDetailList.length === 0">
                        <div class="empty-full">
                            <el-empty description="暂无增值税明细数据" :image-size="160" />
                        </div>
                    </template>
                    <template v-else>
                        <div class="table-wrapper">
                            <el-table :data="vatDetailList" border v-loading="loading" stripe height="100%">
                                <el-table-column prop="orderType" label="类型" width="65" align="center">
                                    <template #default="scope">
                                        <el-tag :type="scope.row.orderType === '进项' ? '' : 'warning'" size="small" effect="plain">
                                            {{ scope.row.orderType }}
                                        </el-tag>
                                    </template>
                                </el-table-column>
                                <el-table-column prop="invoiceNo" label="发票号" min-width="140" show-overflow-tooltip />
                                <el-table-column prop="salesContractNo" label="合同号" min-width="140" show-overflow-tooltip />
                                <el-table-column prop="supplierName" label="供应商" min-width="100" show-overflow-tooltip />
                                <el-table-column prop="customerName" label="客户" min-width="100" show-overflow-tooltip />
                                <el-table-column prop="invoiceDate" label="开票日期" width="110" align="center" />
                                <el-table-column prop="taxRate" label="税率" width="75" align="center">
                                    <template #default="scope">
                                        {{ scope.row.taxRate }}%
                                    </template>
                                </el-table-column>
                                <el-table-column prop="taxAmount" label="税额" width="110" align="right">
                                    <template #default="scope">
                    <span class="tax-amount" :class="scope.row.orderType === '进项' ? 'input-tax' : 'output-tax'">
                      ¥{{ formatNumber(scope.row.taxAmount) }}
                    </span>
                                    </template>
                                </el-table-column>
                            </el-table>
                        </div>
                        <el-pagination
                            v-if="page.total > 0"
                            class="vat-pagination"
                            background
                            layout="total, sizes, prev, pager, next, jumper"
                            :current-page="page.current"
                            :page-sizes="[10, 20, 50]"
                            :page-size="page.size"
                            :total="page.total"
                            @size-change="handleSizeChange"
                            @current-change="handleCurrentChange" />
                    </template>
                </el-card>
            </el-col>
            <!-- 右侧:柱状对比图 -->
            <el-col :span="9" class="vat-col">
                <el-card shadow="never" class="vat-card">
                    <template #header>
                        <span class="card-title">进销项增值税对比</span>
                    </template>
                    <div class="summary-row">
                        <div class="summary-item input-bg">
                            <div class="summary-label">进项税额合计</div>
                            <div class="summary-value">¥{{ formatNumber(inputTotal) }}</div>
                        </div>
                        <div class="summary-item output-bg">
                            <div class="summary-label">销项税额合计</div>
                            <div class="summary-value">¥{{ formatNumber(outputTotal) }}</div>
                        </div>
                        <div class="summary-item diff-bg">
                            <div class="summary-label">差额</div>
                            <div class="summary-value" :class="diffValue >= 0 ? 'positive' : 'negative'">
                                {{ diffValue >= 0 ? '+' : '' }}¥{{ formatNumber(diffValue) }}
                            </div>
                        </div>
                    </div>
                    <div v-if="vatDetailList.length > 0" ref="vatChart" class="chart-container"></div>
                    <div v-else class="chart-empty">暂无数据</div>
                </el-card>
            </el-col>
        </el-row>
    </div>
</template>
<script setup>
import { usePaginationApi } from "@/hooks/usePaginationApi";
import { onMounted, getCurrentInstance } from "vue";
import { getTaxList } from "@/api/procurementManagement/taxComparison";
import { ElMessageBox } from "element-plus";
const { proxy } = getCurrentInstance();
import { ref, reactive, computed, onMounted, nextTick, onBeforeUnmount } from "vue";
import * as echarts from 'echarts';
import { getVatDetail } from "@/api/procurementManagement/taxComparison";
defineOptions({
  name: "增值税比对",
    name: "增值税比对",
});
const {
  loading,
  filters,
  columns,
  dataList,
  pagination,
  getTableData,
  resetFilters,
  onCurrentChange,
} = usePaginationApi(
  getTaxList,
  {
    month: [], // 来票日期
  },
  [
    {
      label: "月份",
      prop: "month",
      align: "center",
    },
    {
      label: "销项税额",
      prop: "jtaxAmount",
      align: "center",
    },
    {
      label: "进项税额",
      prop: "xtaxAmount",
      align: "center",
    },
    {
      label: "销-进",
      prop: "taxAmount",
      align: "center",
    },
  ],
  {}
);
const vatChart = ref(null);
let chartInstance = null;
const changePage = ({ page }) => {
  pagination.currentPage = page;
  onCurrentChange(page);
const queryMonth = ref(new Date().toISOString().slice(0, 7));
const monthOptions = ref([]);
const vatDetailList = ref([]);
const loading = ref(false);
const page = reactive({ current: 1, size: 20, total: 0 });
// 汇总额
const inputTotal = computed(() => {
    return vatDetailList.value
        .filter(i => i.orderType === '进项')
        .reduce((sum, i) => sum + (Number(i.taxAmount) || 0), 0);
});
const outputTotal = computed(() => {
    return vatDetailList.value
        .filter(i => i.orderType === '销项')
        .reduce((sum, i) => sum + (Number(i.taxAmount) || 0), 0);
});
const diffValue = computed(() => outputTotal.value - inputTotal.value);
const formatNumber = (val) => {
    const num = Number(val);
    if (isNaN(num)) return '0.00';
    return num.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
};
// 导出
const handleOut = () => {
  ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      proxy.download("/purchase/report/exportTwo", {}, "增值税比对.xlsx");
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
const loadVatDetail = () => {
    loading.value = true;
    const params = {
        current: page.current,
        size: page.size,
        month: queryMonth.value || undefined,
    };
    getVatDetail(params)
        .then((res) => {
            if (res.code === 200) {
                vatDetailList.value = res.data.records || [];
                page.total = res.data.total || 0;
                nextTick(() => renderChart());
            }
        })
        .finally(() => {
            loading.value = false;
        });
};
const renderChart = () => {
    if (!vatChart.value) return;
    if (chartInstance) {
        chartInstance.dispose();
    }
    chartInstance = echarts.init(vatChart.value);
    const data = vatDetailList.value;
    const invoices = data.map(i => i.invoiceNo);
    chartInstance.setOption({
        tooltip: {
            trigger: 'axis',
            backgroundColor: 'rgba(255,255,255,0.95)',
            borderColor: '#e0e0e0',
            borderWidth: 1,
            textStyle: { color: '#333', fontSize: 13 },
            formatter: function (params) {
                let html = `<b>${params[0].axisValue}</b><br/>`;
                params.forEach(p => {
                    if (p.value > 0) {
                        html += `${p.marker} ${p.seriesName}: ¥${formatNumber(p.value)}<br/>`;
                    }
                });
                return html;
            },
        },
        legend: {
            data: ['进项税额', '销项税额'],
            bottom: 0,
            textStyle: { fontSize: 12 },
        },
        grid: {
            left: '10%',
            right: '8%',
            top: '8%',
            bottom: '12%',
        },
        xAxis: {
            type: 'category',
            data: invoices,
            axisLabel: {
                rotate: 45,
                fontSize: 10,
                interval: 0,
            },
            axisTick: { alignWithLabel: true },
        },
        yAxis: {
            type: 'value',
            name: '税额(元)',
            nameTextStyle: { fontSize: 11 },
            axisLabel: {
                formatter: (val) => val >= 10000 ? `${(val / 10000).toFixed(1)}万` : val,
            },
        },
        series: [
            {
                name: '进项税额',
                type: 'bar',
                barWidth: '35%',
                data: data.map(i => i.orderType === '进项' ? Number(i.taxAmount) || 0 : null),
                itemStyle: {
                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                        { offset: 0, color: '#667eea' },
                        { offset: 1, color: '#764ba2' },
                    ]),
                    borderRadius: [4, 4, 0, 0],
                },
                emphasis: {
                    itemStyle: { color: '#667eea' },
                },
            },
            {
                name: '销项税额',
                type: 'bar',
                barWidth: '35%',
                data: data.map(i => i.orderType === '销项' ? Number(i.taxAmount) || 0 : null),
                itemStyle: {
                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                        { offset: 0, color: '#f093fb' },
                        { offset: 1, color: '#f5576c' },
                    ]),
                    borderRadius: [4, 4, 0, 0],
                },
                emphasis: {
                    itemStyle: { color: '#f093fb' },
                },
            },
        ],
    });
};
const handleMonthChange = () => {
    page.current = 1;
    loadVatDetail();
};
const handleSizeChange = (val) => {
    page.size = val;
    loadVatDetail();
};
const handleCurrentChange = (val) => {
    page.current = val;
    loadVatDetail();
};
const generateMonthOptions = () => {
    const now = new Date();
    const options = [];
    for (let i = 11; i >= 0; i--) {
        const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
        const y = d.getFullYear();
        const m = String(d.getMonth() + 1).padStart(2, '0');
        options.push(`${y}-${m}`);
    }
    monthOptions.value = options;
};
const handleResize = () => {
    if (chartInstance) {
        chartInstance.resize();
    }
};
onMounted(() => {
  getTableData();
    generateMonthOptions();
    loadVatDetail();
    window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
    window.removeEventListener('resize', handleResize);
    if (chartInstance) {
        chartInstance.dispose();
    }
});
</script>
<style lang="scss" scoped>
.table_list {
  margin-top: unset;
.vat-page {
    height: calc(100vh - 84px);
}
</style>
.vat-row {
    height: 100%;
}
.vat-col {
    height: 100%;
}
.vat-card {
    height: 100%;
    display: flex;
    flex-direction: column;
    :deep(.el-card__header) {
        flex-shrink: 0;
    }
    :deep(.el-card__body) {
        flex: 1;
        display: flex;
        flex-direction: column;
        overflow: hidden;
    }
}
.card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.card-title {
    font-size: 15px;
    font-weight: 600;
    color: #303133;
}
.table-wrapper {
    flex: 1;
    overflow: hidden;
}
.vat-pagination {
    flex-shrink: 0;
    margin-top: 12px;
    justify-content: flex-end;
}
.tax-amount {
    font-weight: 600;
    font-family: 'Monaco', 'Menlo', monospace;
    &.input-tax { color: #667eea; }
    &.output-tax { color: #f5576c; }
}
.summary-row {
    display: flex;
    gap: 12px;
    margin-bottom: 16px;
    flex-shrink: 0;
    .summary-item {
        flex: 1;
        padding: 12px 14px;
        border-radius: 8px;
        text-align: center;
        &.input-bg {
            background: linear-gradient(135deg, #f3f0ff 0%, #e8e5ff 100%);
            .summary-value { color: #667eea; }
        }
        &.output-bg {
            background: linear-gradient(135deg, #fff0f3 0%, #ffe0e6 100%);
            .summary-value { color: #f5576c; }
        }
        &.diff-bg {
            background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
            .summary-value {
                &.positive { color: #f5576c; }
                &.negative { color: #22c55e; }
            }
        }
        .summary-label {
            font-size: 12px;
            color: #909399;
            margin-bottom: 4px;
        }
        .summary-value {
            font-size: 18px;
            font-weight: 700;
            font-family: 'Monaco', 'Menlo', monospace;
        }
    }
}
.chart-container {
    flex: 1;
    min-height: 0;
}
.empty-full {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
}
.chart-empty {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #c0c4cc;
    font-size: 13px;
}
</style>