<template>
|
<div class="app-container ledger-view">
|
<div class="left-panel">
|
<div class="tree-toolbar">
|
<el-input
|
v-model="treeKeyword"
|
style="width: calc(100% - 102px)"
|
placeholder="请输入区域名称"
|
clearable
|
prefix-icon="Search"
|
@input="filterTree"
|
@clear="filterTree"
|
/>
|
<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 { 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 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,
|
{
|
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>
|