| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-form :model="filters"> |
| | | <el-form-item label="搜索"> |
| | | <div class="app-container ledger-view"> |
| | | <div class="left-panel"> |
| | | <div class="tree-toolbar"> |
| | | <el-input |
| | | v-model="filters.searchText" |
| | | style="width: 240px" |
| | | placeholder="请输入" |
| | | v-model="treeKeyword" |
| | | style="width: calc(100% - 102px)" |
| | | placeholder="请输入区域名称" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | @change="getTableData" |
| | | prefix-icon="Search" |
| | | @input="filterTree" |
| | | @clear="filterTree" |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <PIMTable :column="columns" /> |
| | | <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: 200px" |
| | | placeholder="请输入规格型号" |
| | | clearable |
| | | @change="getTableData" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="供应商"> |
| | | <el-input |
| | | v-model="filters.supplierName" |
| | | 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="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> |
| | | </div> |
| | | |
| | | <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 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 { |
| | | 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"; |
| | | |
| | | defineOptions({ |
| | | name: "设备台账", |
| | | }); |
| | | |
| | | const { filters, columns, getTableData } = usePaginationApi( |
| | | () => {}, |
| | | 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, |
| | | columns, |
| | | dataList, |
| | | pagination, |
| | | getTableData, |
| | | onCurrentChange, |
| | | } = usePaginationApi( |
| | | getLedgerPage, |
| | | { |
| | | searchText: undefined, |
| | | deviceName: undefined, |
| | | deviceModel: undefined, |
| | | supplierName: undefined, |
| | | entryDate: undefined, |
| | | entryDateStart: undefined, |
| | | entryDateEnd: undefined, |
| | | areaId: undefined, |
| | | areaName: undefined, |
| | | }, |
| | | [ |
| | | { |
| | | label: "所在区域", |
| | | prop: "areaName", |
| | | }, |
| | | { |
| | | label: "设备名称", |
| | | prop: "deviceName", |
| | | }, |
| | | { |
| | | label: "规格型号", |
| | | prop: "deviceModel", |
| | | }, |
| | | { |
| | | label: "设备品牌", |
| | | prop: "deviceBrand", |
| | | }, |
| | | { |
| | | label: "设备类型", |
| | | prop: "type", |
| | | }, |
| | | { |
| | | label: "供应商", |
| | | prop: "supplierName", |
| | | }, |
| | | { |
| | | label: "存放位置", |
| | | prop: "storageLocation", |
| | | }, |
| | | { |
| | | label: "数量", |
| | | prop: "number", |
| | | }, |
| | | { |
| | | label: "录入人", |
| | | prop: "createUser", |
| | | }, |
| | | { |
| | | label: "录入日期", |
| | | prop: "createTime", |
| | | formatData: (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); |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | ] |
| | | ); |
| | | |
| | | 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; |
| | | }; |
| | | |
| | | const add = () => { |
| | | modalRef.value.openModal(); |
| | | }; |
| | | |
| | | const edit = (id) => { |
| | | modalRef.value.loadForm(id); |
| | | }; |
| | | |
| | | const changePage = ({ page, limit }) => { |
| | | pagination.currentPage = page; |
| | | pagination.pageSize = limit; |
| | | onCurrentChange(page); |
| | | }; |
| | | |
| | | const deleteRow = (id) => { |
| | | ElMessageBox.confirm("此操作将永久删除该数据,是否继续?", "提示", { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "warning", |
| | | }).then(async () => { |
| | | const { code } = await delLedger(id); |
| | | if (code == 200) { |
| | | ElMessage({ |
| | | type: "success", |
| | | message: "删除成功", |
| | | }); |
| | | getTableData(); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const changeDaterange = (value) => { |
| | | if (value) { |
| | | filters.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD"); |
| | | filters.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD"); |
| | | } else { |
| | | filters.entryDateStart = undefined; |
| | | filters.entryDateEnd = undefined; |
| | | } |
| | | 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("当前查询结果将被导出,是否确认导出?", "导出", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "取消", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | proxy.download("/device/ledger/export", {}, "设备台账档案.xlsx"); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已取消"); |
| | | }); |
| | | }; |
| | | |
| | | const showQRCode = async (row) => { |
| | | const qrContent = proxy.javaApi + "/device-info?deviceId=" + row.id; |
| | | qrCodeUrl.value = await QRCode.toDataURL(qrContent); |
| | | qrRowData.value = row; |
| | | qrDialogVisible.value = true; |
| | | }; |
| | | |
| | | const downloadQRCode = () => { |
| | | const a = document.createElement("a"); |
| | | a.href = qrCodeUrl.value; |
| | | a.download = `${qrRowData.value.deviceName || "二维码"}.png`; |
| | | a.click(); |
| | | }; |
| | | |
| | | 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> |
| | | .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> |