Industrial-image-management.../src/views/system-resource/device-management/index.vue

1049 lines
27 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="equipment-center-container">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="page-title">
<IconDesktop style="font-size: 24px; margin-right: 12px; color: var(--color-primary);" />
<h1>设备中心</h1>
</div>
<div class="page-description">
管理和监控企业设备资产包括设备信息状态跟踪维护记录等
</div>
</div>
<div class="header-right">
<a-space>
<EquipmentSearch
:loading="loading"
@search="handleSearch"
@reset="handleReset"
/>
<a-button type="primary" @click="handleAdd" size="large">
<template #icon>
<IconPlus />
</template>
新增设备
</a-button>
</a-space>
</div>
</div>
</div>
<!-- 统计卡片 -->
<div class="stats-container">
<a-row :gutter="16">
<a-col :span="6">
<a-card class="stat-card" :bordered="false">
<div class="stat-content">
<div class="stat-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<IconDesktop />
</div>
<div class="stat-info">
<div class="stat-number">{{ pagination.total }}</div>
<div class="stat-label">设备总数</div>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="stat-card" :bordered="false">
<div class="stat-content">
<div class="stat-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
<IconCheckCircle />
</div>
<div class="stat-info">
<div class="stat-number">{{ getNormalCount() }}</div>
<div class="stat-label">正常设备</div>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="stat-card" :bordered="false">
<div class="stat-content">
<div class="stat-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
<IconClockCircle />
</div>
<div class="stat-info">
<div class="stat-number">{{ getInUseCount() }}</div>
<div class="stat-label">使用中</div>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="stat-card" :bordered="false">
<div class="stat-content">
<div class="stat-icon" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);">
<IconExclamationCircle />
</div>
<div class="stat-info">
<div class="stat-number">{{ getMaintenanceCount() }}</div>
<div class="stat-label">维护中</div>
</div>
</div>
</a-card>
</a-col>
</a-row>
</div>
<!-- 数据表格 -->
<a-card class="table-card" :bordered="false">
<template #title>
<div class="card-title">
<span>设备列表</span>
<div class="table-actions">
<a-space>
<a-button type="text" @click="refreshData">
<template #icon>
<IconRefresh />
</template>
刷新
</a-button>
<a-button type="text" @click="exportData">
<template #icon>
<IconDownload />
</template>
导出
</a-button>
</a-space>
</div>
</div>
</template>
<!-- 数据表格 -->
<a-table
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
row-key="equipmentId"
@change="handleTableChange"
>
<template #equipmentType="{ record }">
<a-tag :color="getEquipmentTypeColor(record.equipmentType)">
{{ getEquipmentTypeText(record.equipmentType) }}
</a-tag>
</template>
<template #equipmentStatus="{ record }">
<a-space>
<div
:class="getStatusDotClass(record.equipmentStatus)"
class="status-dot"
/>
<a-tag :color="getEquipmentStatusColor(record.equipmentStatus)">
{{ getEquipmentStatusText(record.equipmentStatus) }}
</a-tag>
</a-space>
</template>
<template #locationStatus="{ record }">
<a-tag :color="getLocationStatusColor(record.locationStatus)">
{{ getLocationStatusText(record.locationStatus) }}
</a-tag>
</template>
<template #healthStatus="{ record }">
<a-tag :color="getHealthStatusColor(record.healthStatus)">
{{ getHealthStatusText(record.healthStatus) }}
</a-tag>
</template>
<template #useStatus="{ record }">
<a-tag :color="record.useStatus === '1' ? 'blue' : 'green'">
{{ record.useStatus === '1' ? '使用中' : '空闲' }}
</a-tag>
</template>
<template #purchasePrice="{ record }">
<span v-if="record.purchasePrice">
¥{{ formatPrice(record.purchasePrice) }}
</span>
<span v-else>-</span>
</template>
<template #currentNetValue="{ record }">
<span v-if="record.currentNetValue">
¥{{ formatPrice(record.currentNetValue) }}
</span>
<span v-else>-</span>
</template>
<template #createTime="{ record }">
{{ formatDateTime(record.createTime) }}
</template>
<template #action="{ record }">
<a-space>
<a-button type="text" size="small" @click="handleView(record)">
查看
</a-button>
<a-button type="text" size="small" @click="handleDetail(record)">
详情
</a-button>
<a-button type="text" size="small" @click="handleEdit(record)">
编辑
</a-button>
<a-button
v-if="record.useStatus === '0'"
type="text"
size="small"
@click="handleAssign(record)"
>
分配
</a-button>
<a-button
v-if="record.useStatus === '1'"
type="text"
size="small"
@click="handleReturn(record)"
>
归还
</a-button>
<a-button
type="text"
size="small"
danger
@click="handleDelete(record)"
>
删除
</a-button>
</a-space>
</template>
</a-table>
</a-card>
<!-- 新增/编辑弹窗 -->
<DeviceModal
v-model:visible="modalVisible"
:equipment-data="currentEquipment"
:mode="modalMode"
@success="handleModalSuccess"
/>
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref, watch } from 'vue'
import { Modal } from '@arco-design/web-vue'
import {
IconCheckCircle,
IconClockCircle,
IconDesktop,
IconDownload,
IconExclamationCircle,
IconPlus,
IconRefresh,
IconSearch,
} from '@arco-design/web-vue/es/icon'
import message from '@arco-design/web-vue/es/message'
import DeviceModal from './components/DeviceModal.vue'
import EquipmentSearch from './components/EquipmentSearch.vue'
import router from '@/router'
import { EquipmentAPI } from '@/apis'
import type { EquipmentPageQuery, EquipmentResp } from '@/types/equipment.d'
defineOptions({ name: 'EquipmentCenter' })
// 当前搜索参数
const currentSearchParams = ref<EquipmentPageQuery>({})
// 表格数据
const tableData = ref<EquipmentResp[]>([])
const loading = ref(false)
// 分页配置
const pagination = reactive<any>({
current: 1,
pageSize: 10,
total: 0,
showPageSize: true,
showJumper: true,
showTotal: (total: number) => `${total} 条记录`,
})
// 弹窗控制
const modalVisible = ref(false)
const currentEquipment = ref<EquipmentResp | null>(null)
const modalMode = ref<'add' | 'edit' | 'view'>('add')
// 表格列配置
const columns = [
{
title: '资产编号',
dataIndex: 'assetCode',
key: 'assetCode',
width: 120,
},
{
title: '设备名称',
dataIndex: 'equipmentName',
key: 'equipmentName',
width: 150,
},
{
title: '设备类型',
dataIndex: 'equipmentType',
key: 'equipmentType',
slotName: 'equipmentType',
width: 120,
},
{
title: '设备型号',
dataIndex: 'equipmentModel',
key: 'equipmentModel',
width: 120,
},
{
title: '序列号',
dataIndex: 'equipmentSn',
key: 'equipmentSn',
width: 150,
},
{
title: '品牌',
dataIndex: 'brand',
key: 'brand',
width: 100,
},
{
title: '设备状态',
dataIndex: 'equipmentStatus',
key: 'equipmentStatus',
slotName: 'equipmentStatus',
width: 120,
},
{
title: '使用状态',
dataIndex: 'useStatus',
key: 'useStatus',
slotName: 'useStatus',
width: 100,
},
{
title: '位置状态',
dataIndex: 'locationStatus',
key: 'locationStatus',
slotName: 'locationStatus',
width: 120,
},
{
title: '物理位置',
dataIndex: 'physicalLocation',
key: 'physicalLocation',
width: 150,
},
{
title: '健康状态',
dataIndex: 'healthStatus',
key: 'healthStatus',
slotName: 'healthStatus',
width: 100,
},
{
title: '负责人',
dataIndex: 'responsiblePerson',
key: 'responsiblePerson',
width: 100,
},
{
title: '当前使用人',
dataIndex: 'name',
key: 'name',
width: 100,
},
{
title: '所属项目',
dataIndex: 'projectName',
key: 'projectName',
width: 150,
},
{
title: '采购价格',
dataIndex: 'purchasePrice',
key: 'purchasePrice',
width: 120,
slotName: 'purchasePrice',
},
{
title: '当前净值',
dataIndex: 'currentNetValue',
key: 'currentNetValue',
width: 120,
slotName: 'currentNetValue',
},
{
title: '维护人员',
dataIndex: 'maintenancePerson',
key: 'maintenancePerson',
width: 100,
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 160,
slotName: 'createTime',
},
{
title: '操作',
key: 'action',
slotName: 'action',
width: 280,
fixed: 'right',
},
]
// 下拉选项
// 获取设备类型文本
const getEquipmentTypeText = (type: string) => {
const typeMap: Record<string, string> = {
detection: '检测设备',
maintain: '维修设备',
security: '安全设备',
office: '办公设备',
car: '车辆',
}
return typeMap[type] || type
}
// 获取设备类型颜色
const getEquipmentTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
detection: 'blue',
maintain: 'green',
security: 'orange',
office: 'purple',
car: 'red',
}
return colorMap[type] || 'default'
}
// 获取设备状态文本
const getEquipmentStatusText = (status: string) => {
const statusMap: Record<string, string> = {
normal: '正常',
repair: '维修中',
maintain: '保养中',
scrap: '报废',
}
return statusMap[status] || status
}
// 获取设备状态颜色
const getEquipmentStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
normal: 'green',
repair: 'orange',
maintain: 'blue',
scrap: 'red',
}
return colorMap[status] || 'default'
}
// 获取位置状态文本
const getLocationStatusText = (status: string) => {
const statusMap: Record<string, string> = {
in_stock: '库存中',
allocated: '已分配',
repair: '维修中',
scrap: '待报废',
scrapped: '已报废',
borrowed: '外借中',
lost: '丢失',
}
return statusMap[status] || status
}
// 获取位置状态颜色
const getLocationStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
in_stock: 'green',
allocated: 'blue',
repair: 'orange',
scrap: 'red',
scrapped: 'red',
borrowed: 'purple',
lost: 'red',
}
return colorMap[status] || 'default'
}
// 获取健康状态文本
const getHealthStatusText = (status: string) => {
const statusMap: Record<string, string> = {
excellent: '优秀',
good: '良好',
normal: '一般',
poor: '较差',
bad: '差',
}
return statusMap[status] || status
}
// 获取健康状态颜色
const getHealthStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
excellent: 'green',
good: 'blue',
normal: 'orange',
poor: 'red',
bad: 'red',
}
return colorMap[status] || 'default'
}
// 获取状态点样式
const getStatusDotClass = (status: string) => {
const classMap: Record<string, string> = {
normal: 'normal-dot',
repair: 'maintenance-dot',
maintain: 'maintenance-dot',
scrap: 'scrapped-dot',
}
return classMap[status] || 'normal-dot'
}
// 格式化价格
const formatPrice = (price: number) => {
if (!price) return '0.00'
return price.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
}
// 格式化日期时间
const formatDateTime = (dateTime: string) => {
if (!dateTime) return '-'
try {
const date = new Date(dateTime)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
// eslint-disable-next-line unused-imports/no-unused-vars
} catch (error) {
return dateTime
}
}
// 数据转换函数
const transformBackendData = (data: any[]) => {
return data.map((item) => {
// 格式化时间字段
const formatDateTime = (dateTime: any) => {
if (!dateTime) return ''
try {
return new Date(dateTime).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
// eslint-disable-next-line unused-imports/no-unused-vars
} catch (error) {
return dateTime
}
}
// 直接返回后端数据,只格式化时间字段
return {
...item,
createTime: formatDateTime(item.createTime),
updateTime: formatDateTime(item.updateTime),
purchaseTime: formatDateTime(item.purchaseTime),
inStockTime: formatDateTime(item.inStockTime),
activationTime: formatDateTime(item.activationTime),
expectedScrapTime: formatDateTime(item.expectedScrapTime),
actualScrapTime: formatDateTime(item.actualScrapTime),
statusChangeTime: formatDateTime(item.statusChangeTime),
warrantyExpireDate: formatDateTime(item.warrantyExpireDate),
lastMaintenanceDate: formatDateTime(item.lastMaintenanceDate),
nextMaintenanceDate: formatDateTime(item.nextMaintenanceDate),
}
})
}
// 加载数据
const loadData = async (searchParams?: EquipmentPageQuery) => {
loading.value = true
try {
const params = {
pageSize: pagination.pageSize,
page: pagination.current,
...(searchParams || {}), // 添加搜索条件
}
const res = await EquipmentAPI.pageEquipment(params)
// 兼容不同的响应格式
if (res.success || res.status === 200 || res.code === 200) {
// 处理数据,确保数据格式正确
let dataList: any[] = []
// 检查不同的数据字段
if (Array.isArray(res.data)) {
dataList = res.data
} else if (res.data && Array.isArray((res.data as any).records)) {
dataList = (res.data as any).records
} else if (res.data && Array.isArray((res.data as any).list)) {
dataList = (res.data as any).list
} else if (res.data && Array.isArray((res.data as any).rows)) {
dataList = (res.data as any).rows
}
if (dataList.length > 0) {
const transformedData = transformBackendData(dataList)
tableData.value = transformedData
} else {
tableData.value = []
}
// 设置总数
pagination.total = (res.data as any)?.total || (res as any).total || dataList.length || 0
} else {
message.error(res.msg || '加载数据失败')
}
} catch (error: any) {
console.error('加载数据失败:', error)
message.error(error?.message || '加载数据失败')
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = (searchParams: EquipmentPageQuery) => {
pagination.current = 1
currentSearchParams.value = { ...searchParams }
loadData(searchParams)
}
// 重置
const handleReset = () => {
pagination.current = 1
currentSearchParams.value = {}
loadData()
}
// 表格变化
const handleTableChange = (pag: any) => {
pagination.current = pag.current || 1
pagination.pageSize = pag.pageSize || 10
loadData(currentSearchParams.value)
}
// 新增
const handleAdd = () => {
modalMode.value = 'add'
currentEquipment.value = null
modalVisible.value = true
}
// 查看
const handleView = (record: EquipmentResp) => {
modalMode.value = 'view'
currentEquipment.value = { ...record }
modalVisible.value = true
}
// 编辑
const handleEdit = (record: EquipmentResp) => {
modalMode.value = 'edit'
currentEquipment.value = { ...record }
modalVisible.value = true
}
// 详情
const handleDetail = (record: EquipmentResp) => {
// 跳转到设备详情页面,并传递 equipmentId
router.push({ name: 'DeviceDetail', params: { id: record.equipmentId } })
}
// 分配
const handleAssign = async (record: EquipmentResp) => {
Modal.confirm({
title: '确认分配',
content: `确定要分配设备"${record.equipmentName}"吗?`,
onOk: async () => {
try {
await EquipmentAPI.assignEquipment(record.equipmentId, 'current-user-id')
message.success('分配成功')
loadData()
} catch (error: any) {
console.error('分配失败:', error)
message.error(error?.message || '分配失败')
}
},
})
}
// 归还
const handleReturn = async (record: EquipmentResp) => {
Modal.confirm({
title: '确认归还',
content: `确定要归还设备"${record.equipmentName}"吗?`,
onOk: async () => {
try {
await EquipmentAPI.returnEquipment(record.equipmentId)
message.success('归还成功')
loadData()
} catch (error: any) {
console.error('归还失败:', error)
message.error(error?.message || '归还失败')
}
},
})
}
// 统计函数
const getNormalCount = () => {
return tableData.value.filter((item) => item.equipmentStatus === 'normal').length
}
const getInUseCount = () => {
return tableData.value.filter((item) => item.useStatus === '1').length
}
const getMaintenanceCount = () => {
return tableData.value.filter((item) =>
item.equipmentStatus === 'repair'
|| item.locationStatus === 'repair',
).length
}
// 刷新数据
const refreshData = () => {
loadData()
message.success('数据已刷新')
}
// 导出数据
const exportData = () => {
message.info('导出功能开发中...')
}
// 删除
const handleDelete = async (record: EquipmentResp) => {
// 检查设备状态,提供更详细的确认信息
let confirmContent = `确定要删除设备"${record.equipmentName}"吗?`
let canDelete = true
let statusWarning = ''
// 检查设备使用状态
if (record.useStatus === '1') {
canDelete = false
statusWarning = '该设备正在使用中,无法删除。请先归还设备后再进行删除操作。'
}
// 检查设备位置状态
if (record.locationStatus === 'allocated' || record.locationStatus === 'borrowed') {
canDelete = false
statusWarning = '该设备已分配或外借中,无法删除。请先归还设备后再进行删除操作。'
}
// 检查设备是否在维修中
if (record.locationStatus === 'repair' || record.equipmentStatus === 'repair') {
canDelete = false
statusWarning = '该设备正在维修中,无法删除。请等待维修完成后再进行删除操作。'
}
// 如果设备状态不允许删除,显示警告信息
if (!canDelete) {
Modal.warning({
title: '无法删除设备',
content: statusWarning,
okText: '确定',
hideCancel: true,
})
return
}
// 构建详细的确认信息
const deviceInfo = [
`设备名称:${record.equipmentName || '无'}`,
`资产编号:${record.assetCode || '无'}`,
`设备型号:${record.equipmentModel || '无'}`,
`序列号:${record.equipmentSn || '无'}`,
`当前状态:${getEquipmentStatusText(record.equipmentStatus || '')}`,
`位置状态:${getLocationStatusText(record.locationStatus || '')}`,
].join('\n')
confirmContent = `确定要删除以下设备吗?\n\n${deviceInfo}\n\n⚠ 删除后设备将被标记为已报废,此操作不可恢复!\n\n删除原因\n• 设备信息本身不可更改,但可以通过删除操作标记为报废\n• 删除后设备将不再出现在正常列表中\n• 历史记录将被保留,不影响其他模块功能`
Modal.confirm({
title: '确认删除设备',
content: confirmContent,
okText: '确认删除',
cancelText: '取消',
okButtonProps: { status: 'danger' },
onOk: async () => {
try {
await EquipmentAPI.deleteEquipment(record.equipmentId)
message.success('设备删除成功')
loadData()
} catch (error: any) {
console.error('删除失败:', error)
// 根据后端返回的错误信息显示具体的错误提示
const errorMessage = error?.response?.data?.msg || error?.message || '删除失败'
message.error(errorMessage)
}
},
})
}
// 弹窗成功回调
const handleModalSuccess = () => {
modalVisible.value = false
loadData()
}
// 监听表格数据变化
watch(tableData, (_newData) => {
// 数据变化监听
}, { deep: true })
// 初始化
onMounted(() => {
loadData()
})
</script>
<style lang="scss" scoped>
.equipment-center-container {
padding: 24px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
// 页面头部样式
.page-header {
background: white;
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
.header-left {
.page-title {
display: flex;
align-items: center;
margin-bottom: 8px;
h1 {
margin: 0;
font-size: 28px;
font-weight: 700;
color: var(--color-text-1);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
}
.page-description {
color: var(--color-text-3);
font-size: 14px;
line-height: 1.6;
}
}
.header-right {
.arco-space {
gap: 12px;
}
}
}
}
// 统计卡片样式
.stats-container {
margin-bottom: 24px;
.stat-card {
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
}
.stat-content {
display: flex;
align-items: center;
padding: 8px 0;
.stat-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
color: white;
font-size: 24px;
}
.stat-info {
flex: 1;
.stat-number {
font-size: 28px;
font-weight: 700;
color: var(--color-text-1);
line-height: 1;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: var(--color-text-3);
font-weight: 500;
}
}
}
}
}
// 表格卡片样式
.table-card {
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
overflow: hidden;
.card-title {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid var(--color-border);
span {
font-size: 18px;
font-weight: 600;
color: var(--color-text-1);
}
.table-actions {
.arco-space {
gap: 8px;
}
}
}
.arco-table {
.arco-table-th {
background: var(--color-fill-2);
font-weight: 600;
color: var(--color-text-1);
}
.arco-table-td {
border-bottom: 1px solid var(--color-border);
}
.arco-table-tr:hover {
background: var(--color-fill-1);
}
}
}
// 状态点样式
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.normal-dot {
background-color: #52c41a;
}
.maintenance-dot {
background-color: #faad14;
}
.scrapped-dot {
background-color: #ff4d4f;
}
// 标签样式优化
.arco-tag {
border-radius: 6px;
font-weight: 500;
font-size: 12px;
padding: 2px 8px;
}
// 按钮样式优化
.arco-btn {
border-radius: 8px;
font-weight: 500;
transition: all 0.2s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
// 分页样式优化
.arco-pagination {
margin-top: 24px;
justify-content: center;
.arco-pagination-item {
border-radius: 6px;
margin: 0 4px;
&.arco-pagination-item-active {
background: var(--color-primary);
border-color: var(--color-primary);
}
}
}
// 响应式设计
@media (max-width: 768px) {
padding: 16px;
.page-header {
padding: 16px;
.header-content {
flex-direction: column;
align-items: flex-start;
gap: 16px;
.header-right {
width: 100%;
.arco-space {
width: 100%;
justify-content: space-between;
}
}
}
}
.stats-container {
.arco-col {
margin-bottom: 16px;
}
}
}
}
</style>