You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
besure_web/src/views/mes/deviceledger/index.vue

1114 lines
37 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="device-ledger-layout">
<ContentWrap class="device-ledger-left">
<div class="tree-header">
<span class="tree-title">{{ t('EquipmentManagement.EquipmentLedger.lineCategory') }}</span>
<div class="tree-header-actions">
<el-button link type="primary" size="small" @click="getTypeTree">
<Icon icon="ep:refresh" /> 刷新
</el-button>
<el-button link type="primary" size="small" @click="handleTreeAdd({ id: 0, parentChain: '0' })">
<Icon icon="ep:plus" /> {{ t('action.add') }}
</el-button>
</div>
</div>
<el-tree v-loading="typeTreeLoading" :data="typeTreeData" node-key="id" highlight-current :props="typeTreeProps"
:default-expanded-keys="typeTreeExpandedKeys" :expand-on-click-node="false" @node-click="handleTypeNodeClick">
<template #default="{ node, data }">
<span class="custom-tree-node">
<span class="tree-node-label">{{ node.label }}</span>
<span v-if="!isAllTypeNode(data)" class="tree-node-actions">
<el-button link type="primary" size="small" @click.stop="handleTreeAdd(data)">
<Icon icon="ep:plus" />
</el-button>
<el-button link type="primary" size="small" @click.stop="handleTreeEdit(data)">
<Icon icon="ep:edit" />
</el-button>
<el-button link type="danger" size="small" @click.stop="handleTreeDelete(data)">
<Icon icon="ep:delete" />
</el-button>
</span>
</span>
</template>
</el-tree>
</ContentWrap>
<!-- 产线分类 表单弹窗 -->
<Dialog :title="treeFormDialogTitle" v-model="treeFormDialogVisible">
<el-form ref="treeFormRef" :model="treeFormData" :rules="treeFormRules" label-width="80px">
<el-form-item :label="t('EquipmentManagement.EquipmentClassification.code')" prop="code">
<el-row :gutter="20" style="width: 100%">
<el-col :span="18">
<el-input v-model="treeFormData.code"
:placeholder="t('EquipmentManagement.EquipmentClassification.placeholderCode')"
:disabled="Boolean(treeFormData.isCode) || treeFormMode === 'update'" />
</el-col>
<el-col :span="6">
<el-switch v-model="treeFormData.isCode" :disabled="treeFormMode === 'update'"
@change="handleTreeCodeAutoChange" />
</el-col>
</el-row>
</el-form-item>
<el-form-item :label="t('EquipmentManagement.EquipmentClassification.name')" prop="name">
<el-input v-model="treeFormData.name"
:placeholder="t('EquipmentManagement.EquipmentClassification.placeholderName')" />
</el-form-item>
<el-form-item :label="t('EquipmentManagement.EquipmentClassification.sort')" prop="sort">
<el-input v-model="treeFormData.sort"
:placeholder="t('EquipmentManagement.EquipmentClassification.placeholderSort')" />
</el-form-item>
<el-form-item :label="t('EquipmentManagement.EquipmentClassification.remark')" prop="remark">
<el-input v-model="treeFormData.remark"
:placeholder="t('EquipmentManagement.EquipmentClassification.placeholderRemark')" />
</el-form-item>
<el-form-item v-if="treeFormMode === 'update'" :label="t('EquipmentManagement.EquipmentLedger.qrcode')" prop="qrcodeUrl">
<div class="tree-qrcode-wrap">
<QrcodeActionCard
:image-url="treeFormData.qrcodeUrl"
:print-id="treeFormData.id"
:print-template-type="6"
:print-title="`${treeFormData.name || '设备类型'}码打印预览`"
:print-paper-width="80"
:print-paper-height="80"
:print-max-width="220"
:empty-text="t('EquipmentManagement.EquipmentLedger.qrcodeEmpty')"
:error-text="t('EquipmentManagement.EquipmentLedger.qrcodeLoadError')"
:refresh-url="getTreeQrcodeRefreshUrl()"
:refresh-disabled="!treeFormData.id || !treeFormData.code"
refresh-confirm-text="确认刷新该设备类型二维码吗?"
:print-data="buildTreeQrcodePrintData()"
@refresh-success="handleTreeQrcodeRefreshSuccess"
/>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" :loading="treeFormLoading" @click="handleTreeFormSubmit">{{ t('common.ok')
}}</el-button>
<el-button @click="treeFormDialogVisible = false">{{ t('common.cancel') }}</el-button>
</template>
</Dialog>
<div class="device-ledger-right">
<ContentWrap>
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="80px">
<el-form-item :label="t('EquipmentManagement.EquipmentLedger.deviceCode')" prop="deviceCode">
<el-input v-model="queryParams.deviceCode"
:placeholder="t('EquipmentManagement.EquipmentLedger.placeholderDeviceCode')" clearable
@keyup.enter="handleQuery" class="!w-240px" />
</el-form-item>
<el-form-item :label="t('EquipmentManagement.EquipmentLedger.deviceName')" prop="deviceName">
<el-input v-model="queryParams.deviceName"
:placeholder="t('EquipmentManagement.EquipmentLedger.placeholderDeviceName')" clearable
@keyup.enter="handleQuery" class="!w-240px" />
</el-form-item>
<el-form-item :label="t('EquipmentManagement.EquipmentLedger.deviceStatus')" prop="deviceStatus">
<el-select v-model="queryParams.deviceStatus"
:placeholder="t('EquipmentManagement.EquipmentLedger.placeholderDeviceStatus')" clearable
class="!w-240px">
<el-option v-for="dict in tzStatusOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item :label="t('EquipmentManagement.EquipmentLedger.deviceType')" prop="deviceType">
<el-tree-select v-model="queryParams.deviceType" :data="deviceTypeTree" :props="treeSelectProps"
check-strictly default-expand-all value-key="id"
:placeholder="t('EquipmentManagement.EquipmentLedger.placeholderDeviceType')" clearable
class="!w-240px" />
</el-form-item>
<el-form-item :label="t('EquipmentManagement.EquipmentLedger.deviceBrand')" prop="deviceBrand">
<el-input v-model="queryParams.deviceBrand"
:placeholder="t('EquipmentManagement.EquipmentLedger.placeholderDeviceBrand')" clearable
@keyup.enter="handleQuery" class="!w-240px" />
</el-form-item>
<el-form-item :label="t('EquipmentManagement.EquipmentLedger.sn')" prop="sn">
<el-input v-model="queryParams.sn"
:placeholder="t('EquipmentManagement.EquipmentLedger.placeholderSn')" clearable
@keyup.enter="handleQuery" class="!w-240px" />
</el-form-item>
<el-form-item :label="t('EquipmentManagement.EquipmentLedger.outgoingTime')" prop="outgoingTime">
<el-date-picker v-model="queryParams.outgoingTime" type="date" value-format="YYYY-MM-DD"
:placeholder="t('EquipmentManagement.EquipmentLedger.placeholderOutgoingTime')" clearable
class="!w-240px" />
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" />
{{ t('common.query') }}
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" />
{{ t('common.reset') }}
</el-button>
<el-button type="primary" plain @click="openForm('create')" v-hasPermi="['mes:device-ledger:create']">
<Icon icon="ep:plus" class="mr-5px" />
{{ t('action.add') }}
</el-button>
<el-button type="danger" plain @click="handleBatchDelete" v-hasPermi="['mes:device-ledger:delete']">
<Icon icon="ep:delete" class="mr-5px" />
{{ t('EquipmentManagement.EquipmentLedger.batchDelete') }}
</el-button>
<el-button type="success" plain @click="handleExport" :loading="exportLoading"
v-hasPermi="['mes:device-ledger:export']">
<Icon icon="ep:download" class="mr-5px" />
{{ t('action.export') }}
</el-button>
<!-- 视图切换按钮 -->
<!-- <el-button
:type="currentView === 'grid' ? 'primary' : 'default'"
:icon="currentView === 'grid' ? Menu : Grid"
@click="toggleView"
class="view-toggle-btn"
>
{{ currentView === 'grid' ? '表格视图' : '九宫格' }}
</el-button>-->
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<div v-show="currentView === 'table'" class="simple-table-view">
<el-table ref="tableRef" v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"
row-key="id" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" fixed="left" reserve-selection />
<el-table-column :label="t('EquipmentManagement.EquipmentLedger.serialNumber')" align="center" width="50"
fixed="left">
<template #default="scope">
{{ (queryParams.pageNo - 1) * queryParams.pageSize + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column :label="t('EquipmentManagement.EquipmentLedger.deviceCode')" align="center"
prop="deviceCode" min-width="160px" sortable />
<el-table-column :label="t('EquipmentManagement.EquipmentLedger.deviceName')" align="center"
prop="deviceName" min-width="140px" sortable />
<el-table-column :label="t('EquipmentManagement.EquipmentLedger.deviceType')" align="center"
prop="deviceType" min-width="110px" sortable>
<template #default="scope">
<el-tag effect="light">
{{ getDeviceTypeName(scope.row.deviceTypeName ?? scope.row.deviceType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="t('EquipmentManagement.EquipmentLedger.deviceStatus')" align="center"
prop="deviceStatus" sortable>
<template #default="scope">
<el-switch :model-value="isDeviceLedgerEnabled(scope.row)"
:loading="Boolean(deviceStatusUpdatingMap[scope.row.id])" inline-prompt
@change="(val) => handleDeviceStatusChange(scope.row, val)" />
</template>
</el-table-column>
<el-table-column :label="t('EquipmentManagement.EquipmentLedger.isSchedueld')" align="center"
prop="isSchedueld" min-width="100px">
<template #default="scope">
<el-tag :type="Number(scope.row.isSchedueld ?? scope.row.isScheduled) === 1 ? 'success' : 'info'"
effect="light">
{{ formatScheduleLabel(scope.row.isSchedueld ?? scope.row.isScheduled) }}
</el-tag>
</template>
</el-table-column>
<el-table-column :label="t('EquipmentManagement.EquipmentLedger.ratedCapacity')" align="center"
prop="ratedCapacity" min-width="120px" />
<el-table-column :label="t('EquipmentManagement.EquipmentLedger.deviceSpec')" align="center"
prop="deviceSpec" />
<el-table-column :label="t('EquipmentManagement.EquipmentLedger.deviceBrand')" align="center"
prop="deviceBrand" min-width="120px" />
<el-table-column :label="t('EquipmentManagement.EquipmentLedger.sn')" align="center"
prop="sn" min-width="140px" />
<!-- <el-table-column :label="t('EquipmentManagement.EquipmentLedger.deviceModel')"
align="center" prop="deviceModel"/>-->
<el-table-column :label="t('EquipmentManagement.EquipmentLedger.outgoingTime')" align="center"
prop="outgoingTime" :formatter="dateFormatter2" width="120px" sortable />
<el-table-column :label="t('EquipmentManagement.EquipmentLedger.factoryEntryDate')" align="center"
prop="factoryEntryDate" :formatter="dateFormatter2" width="120px" sortable />
<!-- <el-table-column :label="t('EquipmentManagement.EquipmentLedger.supplier')" align="center" prop="supplier" width="110px" /> -->
<!-- <el-table-column :label="t('EquipmentManagement.EquipmentLedger.workshop')" align="center" prop="workshop" width="110px" /> -->
<el-table-column :label="t('EquipmentManagement.EquipmentLedger.workshop')" align="center"
prop="workshopName" min-width="150px" sortable />
<el-table-column :label="t('EquipmentManagement.EquipmentLedger.deviceLocation')" align="center"
prop="deviceLocation" min-width="150px" />
<!-- <el-table-column :label="t('EquipmentManagement.EquipmentLedger.systemOrg')" align="center" prop="systemOrg" width="110px" /> -->
<el-table-column :label="t('EquipmentManagement.EquipmentLedger.deviceManagerName')" align="center"
prop="deviceManagerName" width="150px" sortable />
<el-table-column :label="t('EquipmentManagement.EquipmentLedger.remark')" align="center" prop="remark" />
<!-- <el-table-column :label="t('EquipmentManagement.EquipmentLedger.creatorName')" align="center" prop="creatorName" width="150px" sortable />
<el-table-column :label="t('EquipmentManagement.EquipmentLedger.createTime')" align="center" prop="createTime" :formatter="dateFormatter" width="180px" sortable /> -->
<!-- <el-table-column :label="t('EquipmentManagement.EquipmentLedger.updateTime')" align="center" prop="updateTime" :formatter="dateFormatter" width="180px" sortable /> -->
<el-table-column :label="t('EquipmentManagement.EquipmentLedger.operate')" align="center" min-width="160px"
fixed="right">
<template #default="scope">
<el-button link @click="handleDetail(scope.row.id)">
{{ t('EquipmentManagement.EquipmentLedger.detail') }}
</el-button>
<el-button link type="primary" @click="handleEditDetail(scope.row.id)"
v-hasPermi="['mes:device-ledger:update']">
{{ t('EquipmentManagement.EquipmentLedger.edit') }}
</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)"
v-hasPermi="['mes:device-ledger:delete']">
{{ t('EquipmentManagement.EquipmentLedger.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</div>
<!-- 九宫格视图 -->
<div v-show="currentView === 'grid'" class="simple-grid-view">
<div v-if="!list || list.length === 0" class="empty-grid">
<el-empty :description="t('EquipmentManagement.EquipmentLedger.emptyDeviceData')" />
</div>
<div v-else class="grid-container">
<div v-for="(item, index) in list" :key="item.id || index" class="grid-card"
@click="handleView(item, index)">
<!-- 设备状态指示 -->
<div class="status-indicator" :class="`status-${item.deviceStatus}`"></div>
<!-- 设备图标 -->
<div class="card-icon">
<el-icon :size="32" :color="getEquipmentColor(item.type)">
<component :is="getEquipmentIcon(item.type)" />
</el-icon>
</div>
<!-- 设备基本信息 -->
<div class="card-content">
<div class="card-title">{{ item.name }}</div>
<div class="card-code">{{ item.code }}</div>
<div class="card-model">{{ item.model }}</div>
<!-- 设备状态 -->
<div class="card-status">
<el-tag :type="getStatusTag(item.status)" size="small" class="status-tag">
{{ getStatusText(item.status) }}
</el-tag>
</div>
<!-- 运行信息 -->
<div v-if="item.runningHours" class="card-running">
<span>{{ t('EquipmentManagement.EquipmentLedger.runningLabel') }}: {{
formatRunningHours(item.runningHours) }}</span>
</div>
<!-- 位置信息 -->
<div v-if="item.location" class="card-location">
<el-icon :size="12">
<Location />
</el-icon>
<span>{{ item.location }}</span>
</div>
</div>
</div>
</div>
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
:page-sizes="[12, 24, 48, 96]" @pagination="getList" />
<!-- 分页 -->
<!-- <div class="simple-pagination">
<el-pagination
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
:page-sizes="[12, 24, 48, 96]"
layout="total, sizes, prev, pager, next"
:total="total"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>-->
</div>
</ContentWrap>
</div>
</div>
<!-- 表单弹窗添加/修改 -->
<DeviceLedgerForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
import download from '@/utils/download'
import { DeviceLedgerApi, DeviceLedgerVO } from '@/api/mes/deviceledger'
import { DeviceLineApi, DeviceLineTreeVO } from '@/api/mes/deviceline'
import { DeviceTypeApi, DeviceTypeTreeVO } from '@/api/mes/devicetype'
import QrcodeActionCard from '@/components/QrcodeActionCard/index.vue'
import DeviceLedgerForm from './DeviceLedgerForm.vue'
import { getIntDictOptions } from '@/utils/dict'
import { useDictStoreWithOut } from '@/store/modules/dict'
import { ref } from "vue";
import {
Refresh,
Grid,
Menu,
Search,
Location
} from '@element-plus/icons-vue'
import { useRouter } from "vue-router";
const currentView = ref('table') // 'table' 或 'grid'
// 路由
const router = useRouter()
// 查看详情
const handleView = (row) => {
router.push({
path: '/equipment/detail',
query: { id: row.id }
})
}
/** 设备类型 列表 */
defineOptions({ name: 'DeviceLedger' })
const getEquipmentColor = (type) => {
const colorMap = {
'production': '#409eff',
'inspection': '#67c23a',
'packaging': '#e6a23c',
'transport': '#f56c6c',
'other': '#909399'
}
return colorMap[type] || '#409eff'
}
const getEquipmentIcon = (type) => {
const iconMap = {
'production': 'Monitor',
'inspection': 'Search',
'packaging': 'Box',
'transport': 'Truck',
'other': 'Tools'
}
return iconMap[type] || 'Tools'
}
// 工具函数
const getStatusTag = (status) => {
const tagMap = {
'running': 'success',
'standby': 'info',
'fault': 'danger',
'maintenance': 'warning',
'stopped': 'info'
}
return tagMap[status] || 'info'
}
const getStatusText = (status) => {
const textMap = {
'running': t('EquipmentManagement.EquipmentLedger.statusRunning'),
'standby': t('EquipmentManagement.EquipmentLedger.statusStandby'),
'fault': t('EquipmentManagement.EquipmentLedger.statusFault'),
'maintenance': t('EquipmentManagement.EquipmentLedger.statusMaintenance'),
'stopped': t('EquipmentManagement.EquipmentLedger.statusStopped')
}
return textMap[status] || t('EquipmentManagement.EquipmentLedger.statusUnknown')
}
// 分页
const handleSizeChange = (size) => {
queryParams.pageSize = size
getList()
}
const handlePageChange = (page) => {
queryParams.pageNo = page
getList()
}
const formatRunningHours = (hours) => {
if (!hours) return '0h'
if (hours < 24) return `${hours}h`
const days = Math.floor(hours / 24)
const remainingHours = hours % 24
return remainingHours > 0 ? `${days}d ${remainingHours}h` : `${days}d`
}
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref<DeviceLedgerVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const selectedDeviceLineId = ref<number | undefined>(undefined)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
deviceCode: undefined,
deviceName: undefined,
deviceStatus: undefined,
deviceBrand: undefined,
sn: undefined,
outgoingTime: undefined,
deviceType: undefined as number | undefined,
deviceLine: undefined as number | undefined,
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false)
const tableRef = ref()
const selectedIds = ref<number[]>([])
const handleSelectionChange = (rows: any[]) => {
selectedIds.value = rows?.map((row) => row.id).filter((id) => id !== undefined) ?? []
}
// 切换视图
const toggleView = () => {
currentView.value = currentView.value === 'table' ? 'grid' : 'table'
// 保存视图偏好
localStorage.setItem('equipment-view', currentView.value)
}
const dictStore = useDictStoreWithOut()
const dictReady = ref(false)
const tzStatusOptions = computed(() => {
if (!dictReady.value) return []
const options = getIntDictOptions('mes_tz_status')
return options.filter((o) => o.value !== null && o.value !== undefined && !Number.isNaN(o.value))
})
const deviceStatusUpdatingMap = ref<Record<number, boolean>>({})
const typeTreeLoading = ref(false)
const typeTreeData = ref<DeviceLineTreeVO[]>([])
const typeTreeProps = { label: 'name', children: 'children' }
const treeSelectProps = { label: 'name', children: 'children' }
const typeTreeExpandedKeys = ref<number[]>([])
const deviceTypeTree = ref<DeviceTypeTreeVO[]>([])
const deviceTypeNameMap = ref<Record<number, string>>({})
const ALL_TYPE_NODE_ID = -1
const buildDeviceTypeNameMap = (nodes: DeviceTypeTreeVO[]) => {
const map: Record<number, string> = {}
const stack = [...nodes]
while (stack.length) {
const node = stack.pop()!
if (typeof node.id === 'number') map[node.id] = node.name
if (Array.isArray(node.children) && node.children.length) stack.push(...node.children)
}
deviceTypeNameMap.value = map
}
const isAllTypeNode = (node: any) => Number(node?.id) === ALL_TYPE_NODE_ID
const buildTypeTreeWithAll = (nodes: DeviceLineTreeVO[]) => {
return [
{
id: ALL_TYPE_NODE_ID,
code: '',
isCode: false,
name: t('EquipmentManagement.EquipmentLedger.lineCategoryAll'),
qrcodeUrl: '',
remark: '',
sort: 0,
parentId: 0,
parentChain: '0',
children: nodes
}
] as DeviceLineTreeVO[]
}
const getTypeTree = async () => {
typeTreeLoading.value = true
try {
const data = await DeviceLineApi.getDeviceLineTree()
const treeData = JSON.parse(JSON.stringify(data ?? []))
typeTreeData.value = buildTypeTreeWithAll(treeData)
typeTreeExpandedKeys.value = treeData.length > 0 ? [ALL_TYPE_NODE_ID, treeData[0].id] : [ALL_TYPE_NODE_ID]
} finally {
typeTreeLoading.value = false
}
}
const getDeviceTypeTree = async () => {
const data = await DeviceTypeApi.getDeviceTypeTree({})
const treeData = JSON.parse(JSON.stringify(data ?? []))
deviceTypeTree.value = treeData
buildDeviceTypeNameMap(treeData)
}
const handleTypeNodeClick = (node: any) => {
if (isAllTypeNode(node)) {
selectedDeviceLineId.value = undefined
queryParams.deviceLine = undefined
queryParams.pageNo = 1
getList()
return
}
const id = node?.id
selectedDeviceLineId.value = id
queryParams.deviceLine = id
queryParams.pageNo = 1
getList()
}
const treeFormDialogVisible = ref(false)
const treeFormDialogTitle = ref('')
const treeFormLoading = ref(false)
const treeFormMode = ref<'create' | 'update'>('create')
const treeFormRef = ref()
const treeFormData = ref({
id: undefined as number | undefined,
code: '',
isCode: true,
name: '',
qrcodeUrl: '',
remark: '',
sort: '',
parentId: 0,
parentChain: '0',
})
const validateTreeCode = (_rule, value, callback) => {
if (Boolean(treeFormData.value.isCode)) {
callback()
return
}
if (value === undefined || value === null || String(value).trim() === '') {
callback(new Error(t('EquipmentManagement.EquipmentClassification.placeholderCode')))
return
}
callback()
}
const treeFormRules = {
code: [{ validator: validateTreeCode, trigger: ['blur', 'change'] }],
name: [{ required: true, message: t('EquipmentManagement.EquipmentClassification.placeholderName'), trigger: 'blur' }],
sort: [{ required: true, message: t('EquipmentManagement.EquipmentClassification.placeholderSort'), trigger: 'blur' }],
}
const resetTreeForm = () => {
treeFormData.value = {
id: undefined,
code: '',
isCode: true,
name: '',
qrcodeUrl: '',
remark: '',
sort: '',
parentId: 0,
parentChain: '0',
}
treeFormRef.value?.resetFields()
}
const handleTreeCodeAutoChange = (value: boolean) => {
if (value) {
treeFormData.value.code = ''
}
treeFormRef.value?.clearValidate?.('code')
}
const handleTreeAdd = (parent: any) => {
treeFormMode.value = 'create'
treeFormDialogTitle.value = t('EquipmentManagement.EquipmentLedger.createLineCategory')
resetTreeForm()
treeFormData.value.parentId = parent.id ?? 0
treeFormData.value.parentChain = parent.parentChain ? `${parent.parentChain},${parent.id}` : `${parent.id}`
treeFormDialogVisible.value = true
}
const loadTreeFormDetail = async (id: number) => {
const detail = await DeviceLineApi.getDeviceLine(id)
treeFormData.value = {
id: detail.id,
code: detail.code ?? '',
isCode: detail.isCode ?? false,
name: detail.name ?? '',
qrcodeUrl: detail.qrcodeUrl ?? '',
remark: detail.remark ?? '',
sort: String(detail.sort ?? ''),
parentId: detail.parentId ?? 0,
parentChain: detail.parentChain ?? '0',
}
}
const handleTreeEdit = async (data: any) => {
treeFormMode.value = 'update'
treeFormDialogTitle.value = t('EquipmentManagement.EquipmentLedger.updateLineCategory')
resetTreeForm()
treeFormDialogVisible.value = true
treeFormLoading.value = true
try {
await loadTreeFormDetail(data.id)
} catch {
treeFormDialogVisible.value = false
message.error(t('common.queryFail'))
} finally {
treeFormLoading.value = false
}
}
const handleTreeDelete = async (data: any) => {
try {
await message.delConfirm()
await DeviceLineApi.deleteDeviceLine(data.id)
message.success(t('common.delSuccess'))
await getTypeTree()
} catch {
// user cancelled or error
}
}
const handleTreeFormSubmit = async () => {
await treeFormRef.value.validate()
treeFormLoading.value = true
try {
const submitData = {
...treeFormData.value,
sort: Number(treeFormData.value.sort) || 0,
}
if (treeFormMode.value === 'create') {
await DeviceLineApi.createDeviceLine(submitData as any)
} else {
await DeviceLineApi.updateDeviceLine(submitData as any)
}
message.success(treeFormMode.value === 'create' ? t('common.createSuccess') : t('common.updateSuccess'))
treeFormDialogVisible.value = false
await getTypeTree()
} finally {
treeFormLoading.value = false
}
}
const getTreeQrcodeRefreshUrl = () => {
if (!treeFormData.value.id || !treeFormData.value.code) return ''
return `/mes/device-line/regenerate-code?id=${treeFormData.value.id}&code=${encodeURIComponent(String(treeFormData.value.code))}`
}
const handleTreeQrcodeRefreshSuccess = async () => {
if (!treeFormData.value.id) return
treeFormLoading.value = true
try {
await loadTreeFormDetail(treeFormData.value.id)
} catch {
message.error(t('common.queryFail'))
} finally {
treeFormLoading.value = false
}
}
const buildTreeQrcodePrintData = () => {
return {
id: treeFormData.value.id,
deviceCode: treeFormData.value.code,
deviceName: treeFormData.value.name,
qrcodeUrl: treeFormData.value.qrcodeUrl
}
}
const getDeviceTypeName = (value: any) => {
const id = typeof value === 'number' ? value : Number(value)
if (!Number.isNaN(id) && deviceTypeNameMap.value[id]) return deviceTypeNameMap.value[id]
return value ?? ''
}
const formatScheduleLabel = (value: any) => {
return Number(value) === 1
? t('EquipmentManagement.EquipmentLedger.yes')
: t('EquipmentManagement.EquipmentLedger.no')
}
const isDeviceLedgerEnabled = (row: DeviceLedgerVO) => {
return Number((row as any)?.deviceStatus) === 0
}
const handleDeviceStatusChange = async (row: DeviceLedgerVO, value: boolean) => {
if (!row?.id) return
const oldValue = Number((row as any).deviceStatus)
const nextValue = value ? 0 : 1
; (row as any).deviceStatus = nextValue
deviceStatusUpdatingMap.value[row.id] = true
try {
await DeviceLedgerApi.updateDeviceLedger({
id: row.id,
deviceStatus: nextValue
} as DeviceLedgerVO)
message.success(t('common.updateSuccess'))
} catch {
; (row as any).deviceStatus = oldValue
message.error(t('common.updateFail'))
} finally {
deviceStatusUpdatingMap.value[row.id] = false
}
}
const handleDetail = (id: number) => {
router.push({ name: 'MesDeviceLedgerDetail', params: { id } })
}
const handleEditDetail = (id: number) => {
router.push({name: 'MesDeviceLedgerEditDetail', params: {id } })
}
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await DeviceLedgerApi.getDeviceLedgerPage({
pageNo: queryParams.pageNo,
pageSize: queryParams.pageSize,
deviceCode: queryParams.deviceCode,
deviceName: queryParams.deviceName,
deviceStatus: queryParams.deviceStatus,
deviceBrand: queryParams.deviceBrand,
sn: queryParams.sn,
outgoingTime: queryParams.outgoingTime,
deviceType: queryParams.deviceType,
deviceLine: queryParams.deviceLine
})
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id, queryParams.deviceType, selectedDeviceLineId.value)
}
/** 删除按钮操作 */
const buildIdsParam = (ids: number | number[]) => {
return Array.isArray(ids) ? ids.join(',') : String(ids)
}
const handleDelete = async (ids: number | number[]) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
const idsParam = buildIdsParam(ids)
await DeviceLedgerApi.deleteDeviceLedger(idsParam)
message.success(t('common.delSuccess'))
selectedIds.value = []
tableRef.value?.clearSelection?.()
// 刷新列表
await getList()
} catch {
}
}
const handleBatchDelete = async () => {
if (!selectedIds.value.length) {
message.error(t('common.delNoData'))
return
}
await handleDelete(selectedIds.value)
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const params: any = {
...queryParams,
ids: selectedIds.value.length ? selectedIds.value.join(',') : undefined
}
const data = await DeviceLedgerApi.exportDeviceLedger(params)
download.excel(data, `${t('EquipmentManagement.EquipmentLedger.exportFileName')}.xls`)
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await dictStore.setDictMap()
dictReady.value = true
getDeviceTypeTree()
getTypeTree()
getList()
})
</script>
<style lang="scss" scoped>
.device-ledger-layout {
display: flex;
gap: 12px;
}
.device-ledger-left {
width: 280px;
flex: 0 0 auto;
}
.device-ledger-right {
flex: 1;
min-width: 0;
}
.simple-grid-view {
background-color: #fff;
border-radius: 4px;
padding: 16px;
height: calc(100vh - 600px);
overflow-y: auto;
.empty-grid {
// 注意:这里缩进要与上面一致
display: flex;
align-items: center;
justify-content: center;
height: 300px;
}
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
padding: 8px;
.grid-card {
position: relative;
border: 1px solid #ebeef5;
border-radius: 8px;
padding: 20px;
background-color: #fafafa;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 16px;
&:hover {
border-color: #409eff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.1);
transform: translateY(-2px);
background-color: #fff;
}
.status-indicator {
position: absolute;
top: 0;
left: 0;
width: 6px;
height: 100%;
border-radius: 8px 0 0 8px;
&.status-running {
background-color: #67c23a;
}
&.status-standby {
background-color: #909399;
}
&.status-fault {
background-color: #f56c6c;
}
&.status-maintenance {
background-color: #e6a23c;
}
&.status-stopped {
background-color: #909399;
}
}
.card-icon {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
background-color: #f5f7fa;
border-radius: 8px;
}
.card-content {
flex: 1;
min-width: 0;
.card-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-code {
font-size: 12px;
color: #909399;
margin-bottom: 2px;
}
.card-model {
font-size: 13px;
color: #606266;
margin-bottom: 8px;
}
.card-status {
margin-bottom: 8px;
.status-tag {
width: 100%;
justify-content: center;
}
}
.card-running {
font-size: 12px;
color: #606266;
margin-bottom: 4px;
}
.card-location {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #909399;
.el-icon {
color: #909399;
}
}
}
}
}
.simple-pagination {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #ebeef5;
text-align: center;
}
}
// 响应式
@media (max-width: 768px) {
.equipment-simple {
padding: 12px;
.simple-toolbar {
flex-direction: column;
gap: 12px;
.toolbar-left {
width: 100%;
.simple-search {
flex: 1;
width: 100%;
}
.status-filter,
.type-filter {
width: 100px;
}
}
.toolbar-right {
width: 100%;
justify-content: flex-end;
}
}
.simple-grid-view {
.grid-container {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
}
}
}
@media (max-width: 480px) {
.equipment-simple {
.simple-grid-view {
.grid-container {
grid-template-columns: 1fr;
}
}
.simple-toolbar {
.toolbar-left {
flex-wrap: wrap;
.simple-search {
width: 100%;
}
.status-filter,
.type-filter {
flex: 1;
min-width: 120px;
}
}
}
}
}
.tree-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid var(--el-border-color-lighter);
margin-bottom: 4px;
.tree-title {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.tree-header-actions {
display: flex;
align-items: center;
gap: 8px;
}
}
.custom-tree-node {
display: flex;
align-items: center;
justify-content: space-between;
flex: 1;
padding-right: 8px;
.tree-node-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tree-node-actions {
display: none;
flex-shrink: 0;
margin-left: 8px;
}
&:hover .tree-node-actions {
display: flex;
}
}
.tree-qrcode-wrap {
width: 100%;
max-width: 220px;
}
</style>