gaoluyang
8 天以前 11b40328f7aa7599f89189d0ebcbbdf8773f9e1b
src/views/equipmentManagement/ledger/index.vue
@@ -1,107 +1,293 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="设备名称">
  <div class="app-container ledger-view">
    <div class="left-panel">
      <div class="tree-toolbar">
        <el-input
          v-model="filters.deviceName"
          style="width: 240px"
          placeholder="请输入设备名称"
          v-model="treeKeyword"
          style="width: calc(100% - 102px)"
          placeholder="请输入区域名称"
          clearable
          @change="getTableData"
          prefix-icon="Search"
          @input="filterTree"
          @clear="filterTree"
        />
      </el-form-item>
      <el-form-item label="规格型号">
        <el-input
        <el-button type="primary" @click="openAreaDialog('addRoot')">新增区域</el-button>
      </div>
      <div class="tree-actions">
        <el-button link type="primary" @click="resetTreeSelection">全部区域</el-button>
      </div>
      <el-tree
        ref="treeRef"
        v-loading="treeLoading"
        :data="treeData"
        :props="treeProps"
        node-key="id"
        highlight-current
        default-expand-all
        :expand-on-click-node="false"
        :filter-node-method="filterTreeNode"
        class="ledger-tree"
        @node-click="handleTreeNodeClick"
      >
        <template #default="{ node, data }">
          <div class="tree-node">
            <span class="tree-node-content">
              <el-icon class="tree-node-icon">
                <component
                  :is="
                    data.children && data.children.length > 0
                      ? node.expanded
                        ? 'FolderOpened'
                        : 'Folder'
                      : 'Tickets'
                  "
                />
              </el-icon>
              <span class="tree-node-label">{{ data.areaName }}</span>
            </span>
            <div class="tree-node-actions">
              <el-button link type="primary" @click.stop="openAreaDialog('edit', data)">编辑</el-button>
              <el-button link type="primary" @click.stop="openAreaDialog('addChild', data)">新增</el-button>
              <el-button
                v-if="!hasChildren(data)"
                link
                type="danger"
                @click.stop="handleDeleteArea(data)"
              >
                删除
              </el-button>
            </div>
          </div>
        </template>
      </el-tree>
    </div>
    <div class="right-panel">
      <el-form :model="filters" :inline="true">
        <el-form-item label="设备名称">
          <el-input
            v-model="filters.deviceName"
            style="width: 200px"
            placeholder="请输入设备名称"
            clearable
            @change="getTableData"
          />
        </el-form-item>
        <el-form-item label="规格型号">
          <el-input
            v-model="filters.deviceModel"
            style="width: 240px"
            style="width: 200px"
            placeholder="请输入规格型号"
            clearable
            @change="getTableData"
        />
      </el-form-item>
      <el-form-item label="供应商">
        <el-input
          />
        </el-form-item>
        <el-form-item label="供应商">
          <el-input
            v-model="filters.supplierName"
            style="width: 240px"
            style="width: 200px"
            placeholder="请输入供应商"
            clearable
            @change="getTableData"
        />
      </el-form-item>
      <el-form-item label="录入日期:">
        <el-date-picker v-model="filters.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
                        placeholder="请选择" clearable @change="changeDaterange" />
      </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 class="table_list">
      <div class="actions">
        <div></div>
        <div>
          <el-button type="primary" @click="add" icon="Plus"> 新增 </el-button>
          <el-button @click="handleOut" icon="download">导出</el-button>
          <el-button
            type="danger"
            icon="Delete"
            :disabled="multipleList.length <= 0"
            @click="deleteRow(multipleList.map((item) => item.id))"
          >
            批量删除
          </el-button>
          />
        </el-form-item>
        <el-form-item label="录入日期">
          <el-date-picker
            v-model="filters.entryDate"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            type="daterange"
            placeholder="请选择"
            clearable
            @change="changeDaterange"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="getTableData">搜索</el-button>
          <el-button @click="handleResetFilters">重置</el-button>
        </el-form-item>
      </el-form>
      <div class="table_list">
        <div class="actions">
          <div class="actions-tip">
            <span v-if="selectedAreaName">当前区域:{{ selectedAreaName }}</span>
          </div>
          <div>
            <el-button type="primary" icon="Plus" @click="add">新增</el-button>
            <el-button type="info" icon="Upload" @click="handleImport">导入</el-button>
            <el-button icon="download" @click="handleOut">导出</el-button>
            <el-button
              type="danger"
              icon="Delete"
              :disabled="multipleList.length <= 0"
              @click="deleteRow(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"
        />
      </div>
      <PIMTable
        rowKey="id"
        isSelection
        :column="columns"
        :tableData="dataList"
        :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @selection-change="handleSelectionChange"
        @pagination="changePage"
      >
      </PIMTable>
    </div>
    <Modal ref="modalRef" @success="getTableData"></Modal>
    <Modal ref="modalRef" @success="getTableData" />
    <el-dialog
      v-model="areaDialogVisible"
      :title="areaDialogTitle"
      width="480px"
      @close="closeAreaDialog"
    >
      <el-form ref="areaFormRef" :model="areaForm" :rules="areaRules" label-width="88px">
        <el-form-item label="区域名称" prop="areaName">
          <el-input v-model="areaForm.areaName" placeholder="请输入区域名称" />
        </el-form-item>
        <el-form-item label="排序" prop="sort">
          <el-input-number v-model="areaForm.sort" :min="0" :step="1" style="width: 100%" />
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input
            v-model="areaForm.remark"
            type="textarea"
            :rows="4"
            maxlength="200"
            show-word-limit
            placeholder="请输入备注"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitAreaForm">确定</el-button>
          <el-button @click="closeAreaDialog">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <el-dialog v-model="qrDialogVisible" title="二维码" width="300px" draggable>
      <div style="text-align:center;">
        <img :src="qrCodeUrl" alt="二维码" style="width:200px;height:200px;" />
        <div style="margin:10px 0;">
      <div class="qr-dialog">
        <img :src="qrCodeUrl" alt="二维码" class="qr-image" />
        <div class="qr-footer">
          <el-button type="primary" @click="downloadQRCode">下载二维码图片</el-button>
        </div>
      </div>
    </el-dialog>
    <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
      <el-upload
        ref="uploadRef"
        :limit="1"
        accept=".xlsx, .xls"
        :headers="upload.headers"
        :action="upload.url"
        :disabled="upload.isUploading"
        :on-progress="handleFileUploadProgress"
        :on-success="handleFileSuccess"
        :auto-upload="false"
        drag
      >
        <el-icon class="el-icon--upload"><upload-filled /></el-icon>
        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
        <template #tip>
          <div class="el-upload__tip text-center">
            <span>仅允许导入 xls、xlsx 格式文件。</span>
            <el-link
              type="primary"
              :underline="false"
              style="font-size: 12px; vertical-align: baseline; margin-left: 5px"
              @click="importTemplate"
            >
              下载模板
            </el-link>
          </div>
        </template>
      </el-upload>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitFileForm">确定</el-button>
          <el-button @click="upload.open = false">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { usePaginationApi } from "@/hooks/usePaginationApi";
// import { Search } from "@element-plus/icons-vue";
import { getLedgerPage, delLedger } from "@/api/equipmentManagement/ledger";
import { onMounted, getCurrentInstance } from "vue";
import {
  getDeviceAreaTree,
  getDeviceAreaDetail,
  addDeviceArea,
  updateDeviceArea,
  deleteDeviceArea,
} from "@/api/equipmentManagement/deviceArea";
import { onMounted, getCurrentInstance, ref, reactive } from "vue";
import Modal from "./Modal.vue";
import { ElMessageBox, ElMessage } from "element-plus";
import { UploadFilled } from "@element-plus/icons-vue";
import { getToken } from "@/utils/auth";
import dayjs from "dayjs";
import QRCode from "qrcode";
import { ref } from "vue";
defineOptions({
  name: "设备台账",
});
// 表格多选框选中项
const multipleList = ref([]);
const { proxy } = getCurrentInstance();
const modalRef = ref();
const treeRef = ref();
const areaFormRef = ref();
const treeKeyword = ref("");
const treeLoading = ref(false);
const treeData = ref([]);
const selectedAreaName = ref("");
const areaDialogVisible = ref(false);
const areaDialogTitle = ref("新增区域");
const areaDialogMode = ref("addRoot");
const qrDialogVisible = ref(false);
const qrCodeUrl = ref("");
const qrRowData = ref(null);
const uploadRef = ref(null);
const treeProps = {
  children: "children",
  label: "areaName",
};
const upload = reactive({
  open: false,
  title: "",
  isUploading: false,
  headers: { Authorization: "Bearer " + getToken() },
  url: import.meta.env.VITE_APP_BASE_API + "/device/ledger/import",
});
const areaForm = reactive({
  id: undefined,
  areaName: "",
  parentId: undefined,
  sort: 0,
  remark: "",
});
const areaRules = {
  areaName: [{ required: true, message: "请输入区域名称", trigger: "blur" }],
};
const {
  filters,
@@ -109,7 +295,6 @@
  dataList,
  pagination,
  getTableData,
  resetFilters,
  onCurrentChange,
} = usePaginationApi(
  getLedgerPage,
@@ -117,10 +302,17 @@
    deviceName: undefined,
    deviceModel: undefined,
    supplierName: undefined,
    entryDate: undefined,
    entryDateStart: undefined,
    entryDateEnd: undefined,
    areaId: undefined,
    areaName: undefined,
  },
  [
    {
      label: "所在区域",
      prop: "areaName",
    },
    {
      label: "设备名称",
      prop: "deviceName",
@@ -157,39 +349,151 @@
      label: "录入日期",
      prop: "createTime",
      formatData: (v) => {
        if (!v) return '';
        // 如果包含时分秒,只取日期部分
        if (v.includes(' ')) {
          return v.split(' ')[0];
        }
        return v;
        if (!v) return "";
        return v.includes(" ") ? v.split(" ")[0] : v;
      },
    },
      {
         dataType: "action",
         label: "操作",
         align: "center",
         fixed: 'right',
         width: 150,
         operation: [
            {
               name: "编辑",
               clickFun: (row) => {
                  edit(row.id)
               },
            },
            {
               name: "生成二维码",
               clickFun: (row) => {
                  showQRCode(row)
               },
            },
         ],
      },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      width: 150,
      operation: [
        {
          name: "编辑",
          clickFun: (row) => {
            edit(row.id);
          },
        },
        {
          name: "生成二维码",
          clickFun: (row) => {
            showQRCode(row);
          },
        },
      ],
    },
  ]
);
// 多选后做什么
const loadTreeData = async () => {
  treeLoading.value = true;
  try {
    const res = await getDeviceAreaTree();
    treeData.value = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : [];
  } finally {
    treeLoading.value = false;
  }
};
const resetAreaForm = () => {
  areaForm.id = undefined;
  areaForm.areaName = "";
  areaForm.parentId = undefined;
  areaForm.sort = 0;
  areaForm.remark = "";
};
const filterTree = () => {
  treeRef.value?.filter(treeKeyword.value);
};
const filterTreeNode = (value, data) => {
  if (!value) {
    return true;
  }
  return String(data.areaName || "").includes(value);
};
const handleTreeNodeClick = (data) => {
  filters.areaId = data.id;
  filters.areaName = data.areaName;
  selectedAreaName.value = data.areaName || "";
  getTableData();
};
const openAreaDialog = async (mode, row) => {
  areaDialogMode.value = mode;
  areaDialogTitle.value =
    mode === "edit" ? "编辑区域" : mode === "addChild" ? "新增子区域" : "新增区域";
  resetAreaForm();
  areaDialogVisible.value = true;
  if (mode === "addChild") {
    areaForm.parentId = row.id;
    areaForm.sort = 0;
    return;
  }
  if (mode === "edit" && row?.id) {
    const res = await getDeviceAreaDetail(row.id);
    const detail = res?.data || {};
    areaForm.id = detail.id;
    areaForm.areaName = detail.areaName || "";
    areaForm.parentId = detail.parentId;
    areaForm.sort = detail.sort ?? 0;
    areaForm.remark = detail.remark || "";
  }
};
const closeAreaDialog = () => {
  areaDialogVisible.value = false;
  areaFormRef.value?.resetFields();
  resetAreaForm();
};
const submitAreaForm = () => {
  areaFormRef.value?.validate(async (valid) => {
    if (!valid) {
      return;
    }
    const submitData = {
      id: areaForm.id,
      areaName: areaForm.areaName,
      parentId: areaForm.parentId,
      sort: areaForm.sort,
      remark: areaForm.remark,
    };
    const request = areaDialogMode.value === "edit" ? updateDeviceArea : addDeviceArea;
    const { code } = await request(submitData);
    if (code === 200) {
      ElMessage.success(areaDialogMode.value === "edit" ? "修改成功" : "新增成功");
      closeAreaDialog();
      await loadTreeData();
    }
  });
};
const handleDeleteArea = (row) => {
  if (hasChildren(row)) {
    ElMessage.warning("当前区域存在下级区域,不能删除");
    return;
  }
  ElMessageBox.confirm("此操作将删除该设备区域,是否继续?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(async () => {
    const { code } = await deleteDeviceArea([row.id]);
    if (code === 200) {
      ElMessage.success("删除成功");
      if (filters.areaId === row.id) {
        resetTreeSelection();
      }
      await loadTreeData();
    }
  });
};
const hasChildren = (row) => Array.isArray(row?.children) && row.children.length > 0;
const resetTreeSelection = () => {
  treeRef.value?.setCurrentKey(null);
  selectedAreaName.value = "";
  filters.areaId = undefined;
  filters.areaName = undefined;
  getTableData();
};
const handleSelectionChange = (selectionList) => {
  multipleList.value = selectionList;
};
@@ -197,16 +501,19 @@
const add = () => {
  modalRef.value.openModal();
};
const edit = (id) => {
  modalRef.value.loadForm(id);
};
const changePage = ({ page, limit }) => {
  pagination.currentPage = page;
   pagination.pageSize = limit;
  pagination.pageSize = limit;
  onCurrentChange(page);
};
const deleteRow = (id) => {
  ElMessageBox.confirm("此操作将永久删除该文件, 是否继续?", "提示", {
  ElMessageBox.confirm("此操作将永久删除该数据,是否继续?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
@@ -233,14 +540,24 @@
  getTableData();
};
const handleResetFilters = () => {
  filters.deviceName = undefined;
  filters.deviceModel = undefined;
  filters.supplierName = undefined;
  filters.entryDate = undefined;
  filters.entryDateStart = undefined;
  filters.entryDateEnd = undefined;
  getTableData();
};
const handleOut = () => {
  ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
  ElMessageBox.confirm("当前查询结果将被导出,是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      proxy.download(`/device/ledger/export`, {}, "设备台账档案.xlsx");
      proxy.download("/device/ledger/export", {}, "设备台账档案.xlsx");
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
@@ -248,8 +565,7 @@
};
const showQRCode = async (row) => {
  // 直接使用URL,不要用JSON.stringify包装
  const qrContent = proxy.javaApi + '/device-info?deviceId=' + row.id;
  const qrContent = proxy.javaApi + "/device-info?deviceId=" + row.id;
  qrCodeUrl.value = await QRCode.toDataURL(qrContent);
  qrRowData.value = row;
  qrDialogVisible.value = true;
@@ -262,18 +578,141 @@
  a.click();
};
onMounted(() => {
const handleImport = () => {
  upload.title = "设备台账导入";
  upload.open = true;
};
const importTemplate = () => {
  proxy.download("/device/ledger/downloadTemplate", {}, `设备台账导入模板_${new Date().getTime()}.xlsx`);
};
const handleFileUploadProgress = () => {
  upload.isUploading = true;
};
const handleFileSuccess = (response, file) => {
  upload.open = false;
  upload.isUploading = false;
  uploadRef.value?.handleRemove(file);
  proxy.$alert(
    "<div style='overflow:auto;overflow-x:hidden;max-height:70vh;padding:10px 20px 0;'>" +
      response.msg +
      "</div>",
    "导入结果",
    { dangerouslyUseHTMLString: true }
  );
  getTableData();
};
const submitFileForm = () => {
  uploadRef.value?.submit();
};
onMounted(async () => {
  await loadTreeData();
  getTableData();
});
</script>
<style lang="scss" scoped>
.table_list {
  margin-top: unset;
.ledger-view {
  display: flex;
  gap: 20px;
}
.left-panel {
  width: 320px;
  min-width: 320px;
  padding: 16px;
  background: #fff;
  border-radius: 4px;
}
.right-panel {
  flex: 1;
  min-width: 0;
  padding: 16px;
  background: #fff;
  border-radius: 4px;
}
.tree-toolbar {
  display: flex;
  gap: 10px;
  align-items: center;
  margin-bottom: 8px;
}
.tree-actions {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 8px;
}
.ledger-tree {
  height: calc(100vh - 230px);
  overflow-y: auto;
}
.tree-node {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.tree-node-content {
  display: flex;
  align-items: center;
  min-width: 0;
}
.tree-node-icon {
  color: #e6a23c;
  margin-right: 8px;
  font-size: 18px;
}
.tree-node-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tree-node-actions {
  flex-shrink: 0;
}
.table_list {
  margin-top: 0;
}
.actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10px;
  gap: 12px;
}
.actions-tip {
  color: #606266;
  font-size: 14px;
}
.qr-dialog {
  text-align: center;
}
.qr-image {
  width: 200px;
  height: 200px;
}
.qr-footer {
  margin: 10px 0;
}
</style>