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.

793 lines
25 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">
<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"/>
</ContentWrap>
<div class="device-ledger-right">
<ContentWrap>
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true"
label-width="60px">
<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>
<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.deviceModel')"
align="center" prop="deviceModel"/>-->
<!-- <el-table-column :label="t('EquipmentManagement.EquipmentLedger.deviceBrand')" align="center" prop="deviceBrand" /> -->
<el-table-column
:label="t('EquipmentManagement.EquipmentLedger.productionDate')" align="center"
prop="productionDate" :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="openForm('update', 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="暂无设备数据"/>
</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>运行: {{ 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 {DeviceTypeApi, DeviceTypeTreeVO} from '@/api/mes/devicetype'
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': '运行中',
'standby': '待机',
'fault': '故障',
'maintenance': '维修中',
'stopped': '已停用'
}
return textMap[status] || '未知'
}
// 分页
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 queryParams = reactive({
pageNo: 1,
pageSize: 10,
deviceCode: undefined,
deviceName: undefined,
deviceStatus: undefined,
deviceType: 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<DeviceTypeTreeVO[]>([])
const typeTreeProps = {label: 'name', children: 'children'}
const typeTreeExpandedKeys = ref<number[]>([0])
const deviceTypeNameMap = ref<Record<number, string>>({})
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 getTypeTree = async () => {
typeTreeLoading.value = true
try {
const data = await DeviceTypeApi.getDeviceTypeTree({pageNo: 1, pageSize: 10})
const treeChildren = JSON.parse(JSON.stringify(data ?? []))
typeTreeData.value = [{
id: 0,
code: '',
name: '全部',
remark: '',
sort: 0,
children: treeChildren
} as any]
buildDeviceTypeNameMap(treeChildren)
typeTreeExpandedKeys.value = [0]
} finally {
typeTreeLoading.value = false
}
}
const handleTypeNodeClick = (node: any) => {
const id = node?.id
queryParams.deviceType = id === 0 ? undefined : id
queryParams.pageNo = 1
getList()
}
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 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,
deviceType: queryParams.deviceType
})
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)
}
/** 删除按钮操作 */
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('请选择需要删除的数据')
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, '设备台账.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(async () => {
await dictStore.setDictMap()
dictReady.value = true
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;
}
}
}
}
}
</style>