天津_意博凯信
1.备件管理-新增独立备件出库功能入口,支持直接完成备件领用出库操作。
2.设备保养-支持按日期分组折叠展示,分组条目可展开查看明细;单条明细行末尾增加【保养完成】操作按钮,可单独办结单条保养任务;折叠汇总状态下,新增【一键保养完成】批量操作按钮,快速办结当前分组全部保养项。
3.设备巡检-按日期分组折叠展示,分组支持展开查看明细;每条巡检明细后增设【巡检完成】按钮,支持单独办结单条巡检记录;分组折叠未展开状态下,提供【一键巡检完成】批量操作,统一办结该分组下所有巡检任务。
4.增值税对比-优化数据展示逻辑,列表展示各订单增值税明细;页面右侧新增柱状可视化图表,直观对比进销项增值税数据。
5.管理驾驶舱-指标修改:将顶部「销售产品数」调整为「销售订单数」;时间筛选:页面左上角新增日期范围下拉筛选控件,所有统计指标标注当前筛选时段;
已修改17个文件
1464 ■■■■■ 文件已修改
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 433 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/spareParts/index.vue 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/index.vue 208 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/center-bottom.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/center-center.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/center-top.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/index.vue 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/taxComparison/index.vue 479 ●●●●● 补丁 | 查看 | 原始文档 | 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
@@ -59,3 +59,20 @@
        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
@@ -24,7 +24,6 @@
                           :label="tab.label"
                           :value="tab.name" />
        </el-radio-group>
        <!-- 操作按钮区 -->
        <el-space v-if="activeRadio !== 'task'">
          <el-button type="primary"
                     :icon="Plus"
@@ -38,7 +37,9 @@
          <el-button @click="handleOut">导出</el-button>
        </el-space>
      </div>
      <div>
            <!-- 巡检任务 -->
            <div v-show="activeRadio === 'taskManage'">
        <PIMTable :table-loading="tableLoading"
                  :table-data="tableData"
                  :column="tableColumns"
@@ -56,8 +57,6 @@
                  :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"
@@ -67,8 +66,7 @@
                  {{ person }}
                </el-tag>
              </template>
              <span v-else
                    class="no-data">--</span>
                            <span v-else class="no-data">--</span>
            </div>
          </template>
          <template #isEnabled="{ row }">
@@ -79,6 +77,80 @@
          </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>
@@ -86,47 +158,43 @@
    <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 { 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 {
    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 queryParams = reactive({
    taskName: "",
  });
const queryParams = reactive({ taskName: "" });
  // 单选框配置
  const activeRadio = ref("taskManage");
  const radios = reactive([
    { name: "taskManage", label: "巡检任务" },
    { name: "task", label: "巡检记录" },
  ]);
  // 表格数据
  const selectedRows = ref([]);
  const tableData = ref([]);
  const operationsArr = ref([]);
@@ -134,316 +202,165 @@
  const tableLoading = ref(false);
  const total = ref(0);
  const pageNum = ref(1);
  const pageSize = ref(10);
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: "isEnabled",
      label: "是否启用",
      minWidth: 100,
      dataType: "slot",
      slot: "isEnabled",
        prop: "frequencyType", label: "频次", minWidth: 120,
        formatData: params => ({ DAILY: "每日", WEEKLY: "每周", MONTHLY: "每月", QUARTERLY: "季度" }[params] || ""),
    },
    {
      prop: "frequencyType",
      label: "频次",
      minWidth: 120,
      formatData: params => {
        return params === "DAILY"
          ? "每日"
          : params === "WEEKLY"
          ? "每周"
          : params === "MONTHLY"
          ? "每月"
          : params === "QUARTERLY"
          ? "季度"
          : "";
      },
    },
    {
      prop: "frequencyDetail",
      label: "开始日期与时间",
      minWidth: 200,
        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]
        );
            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: "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 getOperationColumn = operations => {
    if (!operations || operations.length === 0) return null;
    const operationConfig = {
      label: "操作",
      width: operations.length > 1 ? 180 : 130,
      fixed: "right",
      align: "center",
      dataType: "action",
      operation: operations
        .map(op => {
    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;
                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),
        }).filter(Boolean),
    };
    };
    return operationConfig;
  };
// 巡检记录 - 日期分组
const inspectionExpandedRowKeys = ref([]);
  onMounted(() => {
    radioChange("taskManage");
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 }));
};
  // 单选变化
const groupedInspectionList = computed(() => groupInspectionByDate(tableData.value));
const onInspectionExpandChange = (row, expandedRows) => {
    inspectionExpandedRowKeys.value = expandedRows.map(r => r.date);
};
// 单条巡检完成
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 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 { /* 取消 */ }
};
onMounted(() => { radioChange("taskManage"); });
  const radioChange = value => {
    inspectionExpandedRowKeys.value = [];
    if (value === "taskManage") {
      const operationColumn = getOperationColumn(["edit"]);
      tableColumns.value = [
        ...columns.value,
        ...(operationColumn ? [operationColumn] : []),
      ];
        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] : []),
      ];
        tableColumns.value = [...taskColumns, ...(operationColumn ? [operationColumn] : [])];
      operationsArr.value = ["upload", "viewFile"];
    }
    pageNum.value = 1;
    pageSize.value = 10;
    getList();
  };
  // 查询操作
  const handleQuery = () => {
    pageNum.value = 1;
    pageSize.value = 10;
    getList();
  };
  // 分页处理
  const handlePagination = val => {
    pageNum.value = val.page;
    pageSize.value = val.limit;
    getList();
  };
  // 获取列表数据
const handleQuery = () => { pageNum.value = 1; getList(); };
const handlePagination = val => { pageNum.value = val.page; pageSize.value = val.limit; getList(); };
  const getList = () => {
    tableLoading.value = true;
    const params = {
      ...queryParams,
      size: pageSize.value,
      current: pageNum.value,
    };
    let apiCall;
    if (activeRadio.value === "task") {
      apiCall = inspectionTaskList(params);
    } else {
      apiCall = timingTaskList(params);
    }
    apiCall
      .then(res => {
    const 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 || [];
        // 处理 inspector 字段,将字符串转换为数组(适用于所有情况)
        tableData.value = rawData.map(item => {
          const processedItem = { ...item };
          processedItem.__raw = { ...item };
          // 处理 inspector 字段
          if (processedItem.inspector) {
            if (typeof processedItem.inspector === "string") {
              // 字符串按逗号分割
              processedItem.inspector = processedItem.inspector
                .split(",")
                .map(s => s.trim())
                .filter(s => s);
                    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;
      });
    }).finally(() => { tableLoading.value = false; });
  };
  // 重置查询
  const resetQuery = () => {
    for (const key in queryParams) {
      if (!["pageNum", "pageSize"].includes(key)) {
        queryParams[key] = "";
      }
    }
    handleQuery();
  };
const resetQuery = () => { queryParams.taskName = ""; handleQuery(); };
  // 新增 / 编辑
  const handleAdd = row => {
    const type = row ? "edit" : "add";
    nextTick(() => {
      formDia.value?.openDialog(type, 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 viewFile = row => {
    nextTick(() => {
      viewFiles.value?.openDialog(row);
    });
  };
  const openUploadDialog = row => {
    nextTick(() => {
      uploadFiles.value?.openDialog(row);
    });
  };
  // 删除操作
  const handleDelete = () => {
    if (!selectedRows.value.length) {
      proxy.$modal.msgWarning("请选择要删除的数据");
      return;
    }
    const deleteIds = selectedRows.value.map(item => item.id);
    proxy.$modal
      .confirm("是否确认删除所选数据项?")
      .then(() => {
        return delTimingTask(deleteIds);
      })
      .then(() => {
        proxy.$modal.msgSuccess("删除成功");
        handleQuery();
      })
      .catch(() => {});
    if (!selectedRows.value.length) { proxy.$modal.msgWarning("请选择要删除的数据"); return; }
    proxy.$modal.confirm("是否确认删除所选数据项?").then(() => delTimingTask(selectedRows.value.map(item => item.id)))
        .then(() => { proxy.$modal.msgSuccess("删除成功"); handleQuery(); }).catch(() => {});
  };
  // 多选变更
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
const handleSelectionChange = selection => { selectedRows.value = selection; };
  // 导出
  const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
    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("已取消");
      });
            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
@@ -105,6 +105,7 @@
              <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>
@@ -112,6 +113,9 @@
              <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
@@ -126,6 +130,60 @@
                </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>
@@ -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);
@@ -391,16 +449,10 @@
  fetchListData();
}
// 分页大小改变
const handleSizeChange = (size) => {
  pagination.size = size;
  pagination.current = 1;
  fetchListData();
}
// 当前页改变
const handleCurrentChange = (current) => {
  pagination.current = current;
// 分页变化
const handleSizeChange = ({ page, limit }) => {
    pagination.current = page;
    pagination.size = limit;
  fetchListData();
}
@@ -517,6 +569,93 @@
  }
};
// 直接出库
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();
src/views/equipmentManagement/upkeep/index.vue
@@ -142,68 +142,91 @@
              </el-button>
              <el-button type="danger"
                         icon="Delete"
                         :disabled="multipleList.length <= 0 || hasFinishedStatus"
                                       :disabled="multipleList.length <= 0"
                         @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,
        }"
                    <el-table
                        :data="groupedDataList"
                        border
                        v-loading="tableLoading"
                    @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)"
                        :expand-row-keys="expandedRowKeys"
                        :row-key="(row) => row.date"
                        @expand-change="onExpandChange"
          >
            新增保养
          </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-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>
          </PIMTable>
                        </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>
@@ -217,6 +240,7 @@
              v-model:visible="fileDialogVisible"
              :record-type="'device_maintenance'"
              :record-id="currentMaintenanceTaskId" />
  </div>
</template>
@@ -240,8 +264,11 @@
    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(() =>
@@ -273,10 +300,56 @@
  const dataList = ref([]);
  const pagination = ref({
    currentPage: 1,
    pageSize: 10,
    pageSize: 100,
    total: 0,
  });
  const multipleList = ref([]);
const tableLoading = ref(false);
const expandedRowKeys = ref([]);
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 groupedDataList = computed(() => groupByDate(dataList.value));
// 单条保养完成
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 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 { /* 取消 */ }
};
const onExpandChange = (row, expandedRows) => {
    expandedRowKeys.value = expandedRows.map(r => r.date);
};
  // 保养任务tab相关变量
  const scheduledFilters = reactive({
@@ -513,6 +586,7 @@
  // 保养记录相关方法(原设备保养页面方法)
  const getTableData = async () => {
    tableLoading.value = true;
    try {
      const params = {
        current: pagination.value.currentPage,
@@ -534,6 +608,8 @@
      }
    } catch (error) {
      console.log(error);
    } finally {
        tableLoading.value = false;
    }
  };
@@ -546,13 +622,15 @@
  };
  const handleSelectionChange = selection => {
    multipleList.value = selection;
  };
  // 检查选中的记录中是否有完结状态的
  const hasFinishedStatus = computed(() => {
    return multipleList.value.some(item => item.status === 1);
    // 从选中的分组行中收集所有子记录ID
    const ids = [];
    selection.forEach(group => {
        if (group.children) {
            group.children.forEach(child => ids.push(child.id));
        }
  });
    multipleList.value = ids;
};
  const changePage = page => {
    pagination.value.currentPage = page.page;
@@ -573,12 +651,8 @@
  };
  const delRepairByIds = async ids => {
    // 检查是否有完结状态的记录
    const hasFinished = multipleList.value.some(item => item.status === 1);
    if (hasFinished) {
      ElMessage.warning("不能删除状态为完结的记录");
      return;
    }
    const idList = Array.isArray(ids) ? ids : [ids];
    if (idList.length === 0) return;
    try {
      await ElMessageBox.confirm("确认删除保养数据, 此操作不可逆?", "警告", {
@@ -587,7 +661,7 @@
        type: "warning",
      });
      const { code } = await delUpkeep(ids);
        const { code } = await delUpkeep(idList);
      if (code === 200) {
        ElMessage.success("删除成功");
        getTableData();
src/views/reportAnalysis/PSIDataAnalysis/components/center-bottom.vue
@@ -132,8 +132,11 @@
  },
}
const dateRange = inject('psiDateRange', null)
const fetchData = () => {
  productInOutAnalysis({ type: productType.value })
    const params = { type: productType.value, ...(dateRange?.value || {}) }
    productInOutAnalysis(params)
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        const list = res.data
src/views/reportAnalysis/PSIDataAnalysis/components/center-center.vue
@@ -31,6 +31,8 @@
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: ['周转天数'] }
@@ -69,7 +71,7 @@
const yAxis1 = [{ type: 'value', axisLabel: { color: '#B8C8E0' } }]
const fetchData = () => {
  productTurnoverDays()
    productTurnoverDays(dateRange?.value || undefined)
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        const list = res.data
src/views/reportAnalysis/PSIDataAnalysis/components/center-top.vue
@@ -36,12 +36,15 @@
const compareClass = (val) => (val >= 0 ? 'compare-up' : 'compare-down')
const dateRange = inject('psiDateRange', null)
const fetchData = () => {
  salesPurchaseStorageProductCount()
    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,
                    name: item.name === '销售产品数' ? '销售订单数' : item.name,
          value: item.value,
          rate: item.rate,
        }))
src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue
@@ -29,6 +29,8 @@
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)
@@ -181,7 +183,7 @@
})
const fetchData = () => {
  rawMaterialPurchaseAmountRatio()
    rawMaterialPurchaseAmountRatio(dateRange?.value || undefined)
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        const items = res.data
src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue
@@ -24,6 +24,8 @@
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'
@@ -153,7 +155,7 @@
})
const fetchData = () => {
  productSalesAnalysis()
    productSalesAnalysis(dateRange?.value || undefined)
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        const items = res.data
src/views/reportAnalysis/PSIDataAnalysis/index.vue
@@ -13,7 +13,22 @@
    <!-- 顶部标题栏 -->
    <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>
    <!-- 主要内容区域 -->
@@ -43,8 +58,9 @@
</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'
@@ -65,6 +81,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
@@ -237,15 +274,36 @@
background-repeat: no-repeat;
display: flex;
align-items: center;
justify-content: 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;
}
.fullscreen-btn {
src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue
@@ -28,6 +28,8 @@
import DateTypeSwitch from '../DateTypeSwitch.vue'
import { customerContributionRanking } from '@/api/viewIndex.js'
const dateRange = inject('psiDateRange', null)
const chartStyle = {
  width: '100%',
  height: '100%',
@@ -287,7 +289,7 @@
}
const fetchCustomerRanking = () => {
  customerContributionRanking({ type: dateType.value })
    customerContributionRanking({ type: dateType.value, ...(dateRange?.value || {}) })
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        dataArr.value = res.data.map(item => ({
src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue
@@ -27,6 +27,8 @@
import DateTypeSwitch from '../DateTypeSwitch.vue'
import { supplierPurchaseRanking } from '@/api/viewIndex.js'
const dateRange = inject('psiDateRange', null)
const chartStyle = {
  width: '100%',
  height: '100%',
@@ -295,7 +297,7 @@
// 供应商采购排名
const fetchSupplierRanking = () => {
  supplierPurchaseRanking({ type: radio1.value })
    supplierPurchaseRanking({ type: radio1.value, ...(dateRange?.value || {}) })
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        dataArr.value = res.data.map(item => ({
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 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: "增值税比对",
});
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",
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());
            }
  })
    .then(() => {
      proxy.download("/purchase/report/exportTwo", {}, "增值税比对.xlsx");
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
        .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);
}
.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>