<template>
|
<div class="app-container">
|
<div class="area-view">
|
<!-- 左侧区域树 -->
|
<div class="area-left">
|
<div class="area-left-header">
|
<el-button type="primary" @click="openAreaDialog('add')" icon="Plus">
|
新增区域
|
</el-button>
|
</div>
|
<div
|
class="all-device-node"
|
:class="{ active: currentAreaId === null }"
|
@click="handleAllDeviceClick"
|
>
|
全部设备
|
</div>
|
<el-tree
|
ref="areaTreeRef"
|
v-loading="areaTreeLoading"
|
:data="areaTree"
|
node-key="id"
|
:props="{ label: 'label', children: 'children' }"
|
:expand-on-click-node="false"
|
default-expand-all
|
draggable
|
highlight-current
|
:current-node-key="currentAreaId"
|
@node-click="handleAreaNodeClick"
|
@node-drop="handleAreaNodeDrop"
|
>
|
<template #default="{ node, data }">
|
<div class="area-tree-node">
|
<span class="area-node-label">{{ data.label }}</span>
|
<span class="area-node-count">{{ data.deviceCount ?? 0 }}</span>
|
<span class="area-node-actions">
|
<el-button
|
link
|
type="primary"
|
size="small"
|
@click.stop="openAreaDialog('addChild', data)"
|
>
|
子级
|
</el-button>
|
<el-button
|
link
|
type="primary"
|
size="small"
|
@click.stop="openAreaDialog('edit', data)"
|
>
|
编辑
|
</el-button>
|
<el-button
|
link
|
type="danger"
|
size="small"
|
@click.stop="handleAreaDelete(node, data)"
|
>
|
删除
|
</el-button>
|
</span>
|
</div>
|
</template>
|
</el-tree>
|
</div>
|
|
<!-- 右侧设备列表 -->
|
<div class="area-right">
|
<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="handleReset">重置</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 type="info" @click="handleImport" icon="Upload">导入</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>
|
</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"
|
>
|
</PIMTable>
|
</div>
|
</div>
|
</div>
|
|
<Modal ref="modalRef" @success="getTableData"></Modal>
|
<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">
|
<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>
|
|
<!-- 详情对话框 -->
|
<el-dialog v-model="detailDialogVisible" title="设备台账详情" width="60%" draggable>
|
<el-descriptions :column="2" border>
|
<el-descriptions-item label="设备名称">{{ detailData.deviceName }}</el-descriptions-item>
|
<el-descriptions-item label="规格型号">{{ detailData.deviceModel }}</el-descriptions-item>
|
<el-descriptions-item label="设备品牌">{{ detailData.deviceBrand }}</el-descriptions-item>
|
<el-descriptions-item label="设备类型">{{ detailData.type }}</el-descriptions-item>
|
<el-descriptions-item label="供应商">{{ detailData.supplierName }}</el-descriptions-item>
|
<el-descriptions-item label="所属区域">{{ detailData.areaName }}</el-descriptions-item>
|
<el-descriptions-item label="存放位置">{{ detailData.storageLocation }}</el-descriptions-item>
|
<el-descriptions-item label="单位">{{ detailData.unit }}</el-descriptions-item>
|
<el-descriptions-item label="数量">{{ detailData.number }}</el-descriptions-item>
|
<el-descriptions-item label="启用折旧">{{ detailData.isDepr === 1 ? '是' : '否' }}</el-descriptions-item>
|
<el-descriptions-item label="每年折旧金额">{{ detailData.annualDepreciationAmount }}</el-descriptions-item>
|
<el-descriptions-item label="含税单价">{{ detailData.taxIncludingPriceUnit }}</el-descriptions-item>
|
<el-descriptions-item label="含税总价">{{ detailData.taxIncludingPriceTotal }}</el-descriptions-item>
|
<el-descriptions-item label="税率(%)">{{ detailData.taxRate }}</el-descriptions-item>
|
<el-descriptions-item label="不含税总价">{{ detailData.unTaxIncludingPriceTotal }}</el-descriptions-item>
|
<el-descriptions-item label="录入日期">{{ detailData.createTime }}</el-descriptions-item>
|
<el-descriptions-item label="预计运行时间">{{
|
detailData.planRuntimeTime ? dayjs(detailData.planRuntimeTime).format('YYYY-MM-DD') : ''
|
}}</el-descriptions-item>
|
<el-descriptions-item label="设备图片" :span="2">
|
<div
|
v-if="detailData.storageBlobVOs && detailData.storageBlobVOs.length > 0"
|
style="display: flex; gap: 10px; flex-wrap: wrap"
|
>
|
<el-image
|
v-for="(file, index) in detailData.storageBlobVOs"
|
:key="index"
|
:src="file.previewURL || file.url"
|
:preview-src-list="detailData.storageBlobVOs.map((u) => u.previewURL || u.url)"
|
:initial-index="index"
|
style="width: 100px; height: 100px"
|
fit="cover"
|
/>
|
</div>
|
<span v-else>无图片</span>
|
</el-descriptions-item>
|
</el-descriptions>
|
<template #footer>
|
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
</template>
|
</el-dialog>
|
|
<!-- 区域新增/编辑弹窗 -->
|
<el-dialog
|
v-model="areaDialogVisible"
|
:title="areaDialogTitle"
|
width="420px"
|
draggable
|
@closed="areaFormRef && areaFormRef.resetFields()"
|
>
|
<el-form :model="areaForm" :rules="areaRules" ref="areaFormRef" label-width="90px">
|
<el-form-item label="上级区域" prop="parentId">
|
<el-tree-select
|
v-model="areaForm.parentId"
|
:data="areaTree"
|
node-key="id"
|
:props="{ label: 'label', children: 'children' }"
|
check-strictly
|
clearable
|
placeholder="不选则为根区域"
|
style="width: 100%"
|
/>
|
</el-form-item>
|
<el-form-item label="区域名称" prop="name">
|
<el-input v-model="areaForm.name" placeholder="请输入区域/车间/位置名称" clearable />
|
</el-form-item>
|
<el-form-item label="排序" prop="sort">
|
<el-input-number v-model="areaForm.sort" :min="0" style="width: 100%" placeholder="可选" />
|
</el-form-item>
|
</el-form>
|
<template #footer>
|
<el-button type="primary" @click="submitAreaForm">确定</el-button>
|
<el-button @click="areaDialogVisible = false">取消</el-button>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup>
|
import { usePaginationApi } from "@/hooks/usePaginationApi";
|
import {
|
getLedgerPage,
|
delLedger,
|
getLedgerById,
|
} from "@/api/equipmentManagement/ledger";
|
import {
|
getDeviceAreaTree,
|
addDeviceArea,
|
editDeviceArea,
|
delDeviceArea,
|
} from "@/api/equipmentManagement/area";
|
import { onMounted, getCurrentInstance, ref, reactive, computed } 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 qrDialogVisible = ref(false);
|
const qrCodeUrl = ref("");
|
const qrRowData = ref(null);
|
|
const detailDialogVisible = ref(false);
|
const detailData = ref({});
|
|
// 区域相关
|
const areaTree = ref([]);
|
const areaTreeLoading = ref(false);
|
const areaTreeRef = ref();
|
const currentAreaId = ref(null);
|
const areaDialogVisible = ref(false);
|
const areaDialogType = ref("add");
|
const areaFormRef = ref();
|
const areaForm = reactive({
|
id: undefined,
|
name: "",
|
parentId: null,
|
sort: 0,
|
});
|
const areaRules = {
|
name: [{ required: true, message: "请输入区域名称", trigger: "blur" }],
|
};
|
const areaDialogTitle = computed(() => {
|
if (areaDialogType.value === "edit") return "编辑区域";
|
if (areaDialogType.value === "addChild") return "新增子区域";
|
return "新增区域";
|
});
|
|
// 导入相关
|
const uploadRef = ref(null);
|
const upload = reactive({
|
open: false,
|
title: "",
|
isUploading: false,
|
headers: { Authorization: "Bearer " + getToken() },
|
url: import.meta.env.VITE_APP_BASE_API + "/device/ledger/import",
|
});
|
|
const {
|
filters,
|
columns,
|
dataList,
|
pagination,
|
getTableData,
|
resetFilters,
|
onCurrentChange,
|
} = usePaginationApi(
|
getLedgerPage,
|
{
|
deviceName: undefined,
|
deviceModel: undefined,
|
supplierName: undefined,
|
entryDateStart: undefined,
|
entryDateEnd: undefined,
|
areaId: undefined,
|
},
|
[
|
{
|
label: "设备名称",
|
prop: "deviceName",
|
},
|
{
|
label: "规格型号",
|
prop: "deviceModel",
|
},
|
{
|
label: "设备品牌",
|
prop: "deviceBrand",
|
},
|
{
|
label: "设备类型",
|
prop: "type",
|
},
|
{
|
label: "所属区域",
|
prop: "areaName",
|
},
|
{
|
label: "供应商",
|
prop: "supplierName",
|
},
|
{
|
label: "存放位置",
|
prop: "storageLocation",
|
},
|
{
|
label: "数量",
|
prop: "number",
|
},
|
{
|
label: "录入人",
|
prop: "createUser",
|
},
|
{
|
label: "录入日期",
|
prop: "createTime",
|
formatData: (v) => {
|
if (!v) return "";
|
if (v.includes(" ")) {
|
return v.split(" ")[0];
|
}
|
return v;
|
},
|
},
|
{
|
dataType: "action",
|
label: "操作",
|
align: "center",
|
fixed: "right",
|
width: 180,
|
operation: [
|
{
|
name: "详情",
|
clickFun: (row) => {
|
handleDetail(row);
|
},
|
},
|
{
|
name: "编辑",
|
clickFun: (row) => {
|
edit(row.id);
|
},
|
},
|
{
|
name: "二维码",
|
clickFun: (row) => {
|
showQRCode(row);
|
},
|
},
|
],
|
},
|
]
|
);
|
|
// 加载区域树
|
const getAreaTree = () => {
|
areaTreeLoading.value = true;
|
getDeviceAreaTree()
|
.then((res) => {
|
areaTree.value = res.data || [];
|
})
|
.finally(() => {
|
areaTreeLoading.value = false;
|
});
|
};
|
|
// 重置搜索与区域筛选
|
const handleReset = () => {
|
currentAreaId.value = null;
|
resetFilters();
|
};
|
|
// 点击"全部设备"
|
const handleAllDeviceClick = () => {
|
currentAreaId.value = null;
|
filters.areaId = undefined;
|
getTableData();
|
};
|
|
// 点击区域节点
|
const handleAreaNodeClick = (data) => {
|
currentAreaId.value = data.id;
|
filters.areaId = data.id;
|
getTableData();
|
};
|
|
// 拖拽调整父子关系
|
const handleAreaNodeDrop = (draggingNode, dropNode, dropType) => {
|
const node = draggingNode.data;
|
let parentId = null;
|
if (dropNode && dropType === "inner") {
|
parentId = dropNode.data.id;
|
} else if (dropNode) {
|
parentId = dropNode.data.parentId ?? null;
|
} else {
|
parentId = null;
|
}
|
if (parentId === node.id) {
|
proxy.$modal.msgWarning("不能将区域挂载到自身");
|
getAreaTree();
|
return;
|
}
|
editDeviceArea({
|
id: node.id,
|
name: node.label,
|
parentId,
|
sort: node.sort,
|
})
|
.then(() => {
|
proxy.$modal.msgSuccess("调整成功");
|
getAreaTree();
|
})
|
.catch(() => {
|
getAreaTree();
|
});
|
};
|
|
// 打开区域弹窗
|
const openAreaDialog = (type, data) => {
|
areaDialogType.value = type;
|
areaForm.id = undefined;
|
areaForm.name = "";
|
areaForm.parentId = null;
|
areaForm.sort = 0;
|
if (type === "addChild" && data) {
|
areaForm.parentId = data.id;
|
} else if (type === "edit" && data) {
|
areaForm.id = data.id;
|
areaForm.name = data.label;
|
areaForm.parentId = data.parentId ?? null;
|
areaForm.sort = data.sort ?? 0;
|
}
|
areaDialogVisible.value = true;
|
};
|
|
// 提交区域表单
|
const submitAreaForm = () => {
|
if (!areaFormRef.value) return;
|
areaFormRef.value.validate((valid) => {
|
if (!valid) return;
|
const payload = {
|
name: areaForm.name,
|
parentId: areaForm.parentId ?? null,
|
sort: areaForm.sort ?? 0,
|
};
|
const api =
|
areaDialogType.value === "edit"
|
? editDeviceArea({ ...payload, id: areaForm.id })
|
: addDeviceArea(payload);
|
api
|
.then(() => {
|
proxy.$modal.msgSuccess("操作成功");
|
areaDialogVisible.value = false;
|
getAreaTree();
|
})
|
.catch(() => {});
|
});
|
};
|
|
// 删除区域
|
const handleAreaDelete = (node, data) => {
|
ElMessageBox.confirm("确认删除该区域吗?", "提示", {
|
confirmButtonText: "确定",
|
cancelButtonText: "取消",
|
type: "warning",
|
})
|
.then(() => delDeviceArea(data.id))
|
.then(() => {
|
proxy.$modal.msgSuccess("删除成功");
|
if (currentAreaId.value === data.id) {
|
currentAreaId.value = null;
|
filters.areaId = undefined;
|
}
|
getAreaTree();
|
getTableData();
|
})
|
.catch(() => {});
|
};
|
|
// 多选后做什么
|
const handleSelectionChange = (selectionList) => {
|
multipleList.value = selectionList;
|
};
|
|
const add = () => {
|
modalRef.value.openModal();
|
};
|
const edit = (id) => {
|
modalRef.value.loadForm(id);
|
};
|
const handleDetail = async (row) => {
|
const { code, data } = await getLedgerById(row.id);
|
if (code == 200) {
|
detailData.value = data;
|
detailDialogVisible.value = true;
|
}
|
};
|
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 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;
|
const qrDataUrl = await QRCode.toDataURL(qrContent, { width: 200, margin: 2 });
|
|
const canvas = document.createElement("canvas");
|
const ctx = canvas.getContext("2d");
|
const qrSize = 200;
|
const textHeight = 30;
|
const padding = 10;
|
canvas.width = qrSize + padding * 2;
|
canvas.height = qrSize + textHeight + padding * 2;
|
|
ctx.fillStyle = "#ffffff";
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
const qrImg = new Image();
|
qrImg.src = qrDataUrl;
|
await new Promise((resolve) => {
|
qrImg.onload = resolve;
|
});
|
ctx.drawImage(qrImg, padding, padding, qrSize, qrSize);
|
|
ctx.fillStyle = "#333333";
|
ctx.font = "bold 14px Arial";
|
ctx.textAlign = "center";
|
ctx.fillText(row.deviceName || "", canvas.width / 2, qrSize + padding + 20);
|
|
qrCodeUrl.value = canvas.toDataURL("image/png");
|
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 = (event, file, fileList) => {
|
upload.isUploading = true;
|
};
|
|
const handleFileSuccess = (response, file, fileList) => {
|
upload.open = false;
|
upload.isUploading = false;
|
proxy.$refs["uploadRef"].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 = () => {
|
proxy.$refs["uploadRef"].submit();
|
};
|
|
onMounted(() => {
|
getAreaTree();
|
getTableData();
|
});
|
</script>
|
|
<style lang="scss" scoped>
|
.area-view {
|
display: flex;
|
}
|
.area-left {
|
width: 320px;
|
flex-shrink: 0;
|
padding: 16px;
|
background: #ffffff;
|
margin-right: 16px;
|
}
|
.area-left-header {
|
margin-bottom: 10px;
|
}
|
.all-device-node {
|
padding: 8px 12px;
|
cursor: pointer;
|
font-size: 14px;
|
border-radius: 4px;
|
margin-bottom: 8px;
|
}
|
.all-device-node:hover {
|
background: #f5f7fa;
|
}
|
.all-device-node.active {
|
background: #ecf5ff;
|
color: #409eff;
|
}
|
.area-tree-node {
|
flex: 1;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
font-size: 14px;
|
padding-right: 4px;
|
}
|
.area-node-count {
|
color: #909399;
|
font-size: 12px;
|
margin-left: 6px;
|
}
|
.area-node-actions {
|
display: none;
|
}
|
.area-tree-node:hover .area-node-actions {
|
display: inline-flex;
|
}
|
.area-right {
|
flex: 1;
|
min-width: 0;
|
padding: 16px;
|
background: #ffffff;
|
}
|
.table_list {
|
margin-top: unset;
|
}
|
.actions {
|
display: flex;
|
justify-content: space-between;
|
margin-bottom: 10px;
|
}
|
</style>
|