mes-vue/src/views/biz/device/index.vue
2026-07-28 09:00:22 +08:00

730 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="page-container">
<n-card>
<!-- 搜索表单 -->
<div class="search-form">
<n-form inline :model="searchForm" label-placement="left">
<n-form-item label="主键ID">
<n-input v-model:value="searchForm.deviceCode" placeholder="请输入设备编码" clearable/>
</n-form-item>
<n-form-item>
<n-space>
<n-button type="primary" @click="handleSearch">
<template #icon>
<n-icon>
<SearchOutline/>
</n-icon>
</template>
搜索
</n-button>
<n-button @click="handleReset">
<template #icon>
<n-icon>
<RefreshOutline/>
</n-icon>
</template>
重置
</n-button>
</n-space>
</n-form-item>
</n-form>
</div>
<!-- 工具栏 -->
<div class="table-toolbar">
<n-space>
<n-button type="primary" @click="handleAdd">
<template #icon>
<n-icon>
<AddOutline/>
</n-icon>
</template>
新增设备
</n-button>
<n-button @click="goRealtimeBoard">
设备状态看板
</n-button>
<!-- <n-button @click="importModalVisible = true">
<template #icon><n-icon><CloudUploadOutline /></n-icon></template>
导入
</n-button> -->
<n-button @click="handleExport">
<template #icon>
<n-icon>
<DownloadOutline/>
</n-icon>
</template>
导出{{ selectedIds.length > 0 ? `(${selectedIds.length})` : '' }}
</n-button>
<n-button type="error" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
<template #icon>
<n-icon>
<TrashOutline/>
</n-icon>
</template>
删除
</n-button>
</n-space>
</div>
<!-- 表格 -->
<n-data-table
:columns="columns"
:data="tableData"
:loading="loading"
:row-key="(row) => row.id"
:scroll-x="1200"
v-model:checked-row-keys="selectedIds"
/>
<div class="pagination-container" style="display: flex; justify-content: flex-end; margin-top: 12px">
<n-pagination
v-model:page="pagination.page"
v-model:page-size="pagination.pageSize"
:item-count="pagination.itemCount"
:page-sizes="[10, 20, 50, 100]"
show-size-picker
show-quick-jumper
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
@update:checked-row-keys="handleCheck"
>
<template #prefix>
共 {{ pagination.itemCount }} 条
</template>
</n-pagination>
</div>
</n-card>
<!-- 新增/编辑弹窗 -->
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 600px">
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="100px">
<n-form-item label="设备名称" path="deviceName">
<n-input v-model:value="formData.deviceName" placeholder="请输入设备名称"/>
</n-form-item>
<n-form-item label="新代设备ID" path="synEquipId">
<n-input-number
v-model:value="formData.synEquipId"
placeholder="SynFactory equipID 123"
:min="1"
:show-button="false"
style="width: 100%"
clearable
/>
</n-form-item>
<n-form-item label="所属工段" path="sectionId">
<n-select
v-model:value="formData.sectionId"
:options="sectionList"
placeholder="请选择所属工段"
clearable
style="width: 200px"
/>
</n-form-item>
<n-form-item label="设备类型" path="deviceTypeId">
<n-select
v-model:value="formData.deviceTypeId"
:options="deviceTypeList"
placeholder="请选择设备类型"
clearable
style="width: 200px"
/>
</n-form-item>
<n-form-item label="设备负责人" path="deviceManager">
<n-select
v-model:value="formData.deviceManagers"
:options="deviceManagerList"
multiple
placeholder="请选择设备负责人"
clearable
style="width: 200px"
/>
</n-form-item>
<n-form-item label="生产厂家" path="manufacturer">
<n-input v-model:value="formData.manufacturer" placeholder="请输入生产厂家"/>
</n-form-item>
<n-form-item label="出厂日期" path="manufactureDate">
<n-date-picker v-model:value="formData.manufactureDate" type="datetime" clearable style="width: 100%"/>
</n-form-item>
<n-form-item label="备注" path="remark">
<n-input v-model:value="formData.remark" type="textarea" placeholder="请输入备注"/>
</n-form-item>
<n-form-item label="状态" path="status">
<n-switch v-model:value="formData.status" :checked-value="1" :unchecked-value="0">
<template #checked>启用</template>
<template #unchecked>禁用</template>
</n-switch>
</n-form-item>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="modalVisible = false">取消</n-button>
<n-button type="primary" @click="handleSubmit">确定</n-button>
</n-space>
</template>
</n-modal>
<!-- 导入弹窗 -->
<n-modal v-model:show="importModalVisible" preset="card" title="导入设备表" style="width: 500px">
<n-space vertical>
<n-alert type="info">
<template #header>导入说明</template>
<ul style="margin: 0; padding-left: 16px; line-height: 1.8">
<li>请先下载导入模板,按模板格式填写数据</li>
<li>支持 .xlsx 或 .xls 格式</li>
</ul>
</n-alert>
<n-space>
<n-button type="primary" @click="handleDownloadTemplate">
<template #icon>
<n-icon>
<DownloadOutline/>
</n-icon>
</template>
下载模板
</n-button>
</n-space>
<n-upload :max="1" accept=".xlsx,.xls" :show-file-list="true" :custom-request="handleImportUpload">
<n-upload-dragger>
<div style="margin-bottom: 12px">
<n-icon size="48" :depth="3">
<CloudUploadOutline/>
</n-icon>
</div>
<n-text style="font-size: 16px">点击或拖拽文件到此处上传</n-text>
<n-p depth="3" style="margin: 8px 0 0 0">支持 .xlsx 或 .xls 格式</n-p>
</n-upload-dragger>
</n-upload>
</n-space>
<template #footer>
<n-button @click="importModalVisible = false">关闭</n-button>
</template>
</n-modal>
<!--异常记录抽屉-->
<n-drawer v-model:show="deviceAbnormalRecordDrawerVisible" :title="title" width="1800">
<n-drawer-content :title="title">
<DeviceAbnormalRecordPage
:deviceId="deviceId"
/>
<template #footer>
<n-button @click="doShowInner">
取消
</n-button>
</template>
</n-drawer-content>
</n-drawer>
<!--操作日志抽屉-->
<n-drawer v-model:show="deviceOperationLogDrawerVisible" width="1800">
<n-drawer-content :title="title">
<DeviceOperationLogPage
:deviceId = deviceId
/>
<template #footer>
<n-button @click="doShowInner">
取消
</n-button>
</template>
</n-drawer-content>
</n-drawer>
<!--下发记录抽屉-->
<n-drawer v-model:show="deviceDispatchLogDrawerVisible" width="1800">
<n-drawer-content :title="title">
<DeviceDispatchLogPage
:deviceId = deviceId
/>
<template #footer>
<n-button @click="doShowInner">
取消
</n-button>
</template>
</n-drawer-content>
</n-drawer>
</div>
</template>
<script setup lang="ts">
import {ref, reactive, h, onMounted} from 'vue'
import {useRouter} from 'vue-router'
import {
NButton, NSpace, NTag, NIcon, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions,
NDropdown
} from 'naive-ui'
import {
SearchOutline,
RefreshOutline,
AddOutline,
TrashOutline,
CreateOutline,
CloudUploadOutline,
DownloadOutline, EllipsisHorizontalOutline
} from '@vicons/ionicons5'
import {deviceApi, type Device} from '@/api/device'
import {dictDataApi} from '@/api/org'
import {sectionApi} from '@/api/section'
import {SysUser, userApi} from '@/api/system'
import {DeviceAbnormalRecord, deviceAbnormalRecordApi} from "@/api/deviceAbnormalRecord.ts";
import DeviceAbnormalRecordPage from '@/views/biz/device/DeviceAbnomarlRecord.vue'
import DeviceOperationLogPage from "@/views/biz/device/DeviceOperationLog.vue";
import DeviceDispatchLogPage from "@/views/biz/device/DeviceDispatchLog.vue";
const message = useMessage()
const dialog = useDialog()
const router = useRouter()
//设备Id
const deviceId = ref<number>(undefined)
//设备负责人列表
const deviceManagerList = ref<{ label: string, value: any }[]>([])
let sectionList = reactive<{ label: string, value: any }[]>([])
//设备异常记录
const deviceErrorLogList = ref<DeviceAbnormalRecord[]>([])
const deviceAbnormalRecordDrawerVisible = ref<Boolean>(false) //异常记录抽屉显示
const title = ref('')
//设备下发记录
const deviceIssueRecordList = ref<[]>([])
const deviceDispatchLogDrawerVisible = ref<Boolean>(false)
//操作日志抽屉
const deviceOperationLogDrawerVisible = ref<Boolean>(false)
// 搜索表单
const searchForm = reactive({
deviceCode: null as number | null,
})
// 表格数据
const tableData = ref<Device[]>([])
const loading = ref(false)
const selectedIds = ref<number[]>([])
const pagination = reactive({
page: 1,
pageSize: 10,
itemCount: 0,
showSizePicker: true,
pageSizes: [10, 20, 50]
})
// 弹窗
const modalVisible = ref(false)
const modalTitle = ref('')
const importModalVisible = ref(false)
const formRef = ref()
const defaultFormData: Device = {
id: "",
deviceCode: '',
deviceName: '',
deviceTypeId: undefined,
deviceType: "",
sectionId: "",
sectionName: "",
spec: '',
manufacturer: '',
manufactureDate: undefined,
workshopId: '',
remark: '',
status: 0,
deviceFlag: 'mes_equipment',
synEquipId: null as number | null,
deviceManagerList: null as any | null,
deviceManagers: null as [string] | null,
deviceManager: null as String | null
}
const formData = reactive<Device>({...defaultFormData})
// 字典选项(下拉框/单选框/复选框关联字典时使用)
const deviceTypeList = ref<{ label: string; value: any; class: any }[]>([])
const statusList = ref<{ label: string; value: any; class: any }[]>([])
const deviceFlagList = ref<{ label: string; value: any; class: any }[]>([])
// 表单校验规则
const formRules = {
deviceName: [{required: true, message: '请输入设备名称', trigger: 'blur'},],
sectionId: [{
required: true,
validator(rule: any, value: any) {
if (!value) {
return new Error(`请选择所属工段`)
}
return true
},
trigger: ['blur']
}]
}
// 表格列
const columns: DataTableColumns<Device> = [
{type: 'selection'},
{title: '设备编码', key: 'deviceCode'},
{title: '设备名称', key: 'deviceName'},
{
title: '新代ID', key: 'synEquipId', width: 90,
render: (row) => row.synEquipId ?? '-'
},
{title: '设备类型', key: 'deviceType'},
{title: '设备负责人', key: 'deviceManagerName'},
{title: '所属工段', key: 'sectionName'},
{title: '设备型号', key: 'spec'},
{title: '生产厂家', key: 'manufacturer'},
{title: '出厂日期', key: 'manufactureDate'},
{
title: '状态', key: 'status',
render: (row) => {
const val = row.status
const opt = statusList.value.find(o => o.value === val || String(o.value) === String(val))
if (!opt) return val ?? '-'
return h(NTag, {type: opt.class, size: 'small'}, {default: () => opt.label})
}
},
{
title: '操作',
key: 'actions',
width: 140,
fixed: 'right',
render(row) {
return h('div', {style: {display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'nowrap'}}, [
h(NButton, {size: 'small', quaternary: true, onClick: () => handleEdit(row)}, {
default: () => [h(NIcon, null, {default: () => h(CreateOutline)}), ' 编辑']
}),
h(NButton, {size: 'small', quaternary: true, type: 'error', onClick: () => handleDelete(row)}, {
default: () => [h(NIcon, null, {default: () => h(TrashOutline)}), ' 删除']
}),
h(NDropdown, {
trigger: 'hover',
options: [
{
label: '设备异常记录',
key: 'deviceErrorLog',
},
{
label: '设备下发记录',
key: 'deviceDispatchLog',
},
{
label: '设备操作日志',
key: 'deviceOperationLog',
}
], onSelect: (key: string) => {
switch (key) {
case "deviceErrorLog":
handleDeviceErrorLog(row)
break
case "deviceDispatchLog":
handleDeviceDispatchLog(row)
break
case "deviceOperationLog":
deviceOperationLog(row)
break
}
}
}, {
// 下拉触发按钮:三点图标 / 更多文字
default: () => h(NButton, {size: 'small', quaternary: true}, {
default: () => [h(NIcon, null, {default: () => h(EllipsisHorizontalOutline)})]
})
})
]
)
}
}
]
// 加载数据
async function loadData() {
loading.value = true
try {
const res = await deviceApi.page({
page: pagination.page,
pageSize: pagination.pageSize,
deviceCode: searchForm.deviceCode,
deviceFlag: "mes_equipment"
})
tableData.value = res.list
pagination.itemCount = res.total
} finally {
loading.value = false
}
}
// 搜索
function handleSearch() {
pagination.page = 1
loadData()
}
// 重置
function handleReset() {
searchForm.deviceCode = null
handleSearch()
}
// 分页
function handlePageChange(page: number) {
pagination.page = page
loadData()
}
function handlePageSizeChange(pageSize: number) {
pagination.pageSize = pageSize
pagination.page = 1
loadData()
}
// 选择
function handleCheck(keys: Array<string | number>) {
selectedIds.value = keys as number[]
}
// 新增
function handleAdd() {
modalTitle.value = '新增设备表'
Object.assign(formData, defaultFormData)
modalVisible.value = true
}
function goRealtimeBoard() {
router.push('/biz/device/board')
}
// 编辑
function handleEdit(row: Device) {
Object.assign(formData, defaultFormData)
modalTitle.value = '编辑设备表'
Object.assign(formData, row)
const userIdList = []
if (row.deviceManager) {
let manager = row.deviceManager.split(',');
manager.forEach(item => {
userIdList.push(Number(item))
})
}
formData.deviceManagers = userIdList
modalVisible.value = true
}
// 提交
async function handleSubmit() {
await formRef.value?.validate()
try {
const submitData = {...formData} as Device
if (typeof submitData.manufactureDate === 'number') {
submitData.manufactureDate = new Date(submitData.manufactureDate).toISOString().slice(0, 19).replace('T', ' ')
}
if (submitData.id) {
await deviceApi.update(submitData)
message.success('修改成功')
} else {
await deviceApi.create(submitData)
message.success('新增成功')
}
modalVisible.value = false
loadData()
} catch (error) {
// 错误已在拦截器处理
}
}
// 删除
function handleDelete(row: Device) {
dialog.warning({
title: '提示',
content: '确定要删除该记录吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await deviceApi.delete([row.id!])
message.success('删除成功')
loadData()
} catch (error) {
// 错误已在拦截器处理
}
}
})
}
// 批量删除
function handleBatchDelete() {
dialog.warning({
title: '提示',
content: `确定要删除选中的 ${selectedIds.value.length} 条记录吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await deviceApi.delete(selectedIds.value)
message.success('删除成功')
selectedIds.value = []
loadData()
} catch (error) {
// 错误已在拦截器处理
}
}
})
}
// 导出
async function handleExport() {
try {
const params: Record<string, any> = {}
if (selectedIds.value.length > 0) params.ids = selectedIds.value
if (searchForm.deviceCode != null) params.deviceCode = searchForm.deviceCode
params.deviceFlag = "mes_equipment"
const blob = await deviceApi.export(params)
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '设备表数据.xlsx'
link.click()
window.URL.revokeObjectURL(url)
} catch (error) {
// 错误已在拦截器处理
}
}
// 下载导入模板
async function handleDownloadTemplate() {
try {
const blob = await deviceApi.downloadTemplate()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '设备表导入模板.xlsx'
link.click()
window.URL.revokeObjectURL(url)
} catch (error) {
// 错误已在拦截器处理
}
}
// 导入上传
async function handleImportUpload({file}: UploadCustomRequestOptions) {
if (!file.file) return
try {
const result = await deviceApi.importData(file.file)
if (result.fail > 0) {
dialog.warning({
title: '导入结果',
content: `成功: ${result.success} 条,失败: ${result.fail} 条\n错误信息: ${(result.errors || []).join('\n') || '无'}`,
positiveText: '确定'
})
} else {
message.success(`导入成功,共 ${result.success} 条数据`)
importModalVisible.value = false
}
loadData()
} catch (error) {
// 错误已在拦截器处理
}
}
// 加载字典选项
async function loadDictOptions() {
try {
const data = await dictDataApi.listByType("device_type")
deviceTypeList.value = data.map(d => ({
label: d.dictLabel,
value: (Number(d.dictValue) || d.dictValue),
class: d.listClass
}))
} catch {
}
try {
const data = await dictDataApi.listByType("sys_status")
statusList.value = data.map(d => ({
label: d.dictLabel,
value: (Number(d.dictValue) || d.dictValue),
class: d.listClass
}))
} catch {
}
try {
const data = await dictDataApi.listByType("mes_device_flag")
deviceFlagList.value = data.map(d => ({
label: d.dictLabel,
value: (Number(d.dictValue) || d.dictValue),
class: d.listClass
}))
} catch {
}
}
//加载工段
async function loadSection() {
const data = await sectionApi.listNoParam()
sectionList = data.map((d: any) => ({label: d.sectionName, value: (Number(d.id) || d.id)}))
}
//加载人员
async function handleUser() {
const res = await userApi.getPathList()
deviceManagerList.value = res.map((n:any)=>{
return {
label:n.username,
value:n.id
}
})
}
//查看设备异常记录
async function handleDeviceErrorLog(row: Device) {
deviceId.value = row.id
deviceAbnormalRecordDrawerVisible.value = true
title.value = row.deviceName+"异常记录"
}
async function handleDeviceDispatchLog(row: Device) {
deviceId.value = row.id
deviceDispatchLogDrawerVisible.value = true
title.value = row.deviceName+"设备下发记录"
}
async function deviceOperationLog(row: Device) {
deviceId.value = row.id
deviceOperationLogDrawerVisible.value = true
title.value = row.deviceName + "操作记录";
}
function doShowInner() {
deviceAbnormalRecordDrawerVisible.value = false
deviceOperationLogDrawerVisible.value = false
deviceDispatchLogDrawerVisible.value = false
}
onMounted(() => {
loadData()
handleUser()
loadSection()
loadDictOptions()
})
</script>
<style scoped>
.search-form {
margin-bottom: 16px;
}
.table-toolbar {
margin-bottom: 16px;
}
</style>