实现设备中心添加设备类型弹窗页面以及另外搭建完成设备盘库页面

This commit is contained in:
Mr.j 2025-08-13 10:17:19 +08:00
parent b1b34e630f
commit bac2e99f0d
7 changed files with 622 additions and 12 deletions

View File

@ -49,4 +49,24 @@ export function returnEquipment(equipmentId: string) {
}
// 导出设备采购 API
export * from './procurement'
export * from './procurement'
// 设备盘库相关 API
/** @desc 分页查询设备盘库记录 */
export function pageEquipmentInventory(query: T.EquipmentPageQuery) {
return http.get<T.EquipmentResp[]>(`${BASE_URL}/inventory/page`, query)
}
/** @desc 执行设备盘库 */
export function executeEquipmentInventory(equipmentId: string, inventoryResult: string, remark?: string) {
return http.post(`${BASE_URL}/inventory/${equipmentId}`, null, {
params: { inventoryResult, remark }
})
}
/** @desc 批量执行设备盘库 */
export function batchExecuteEquipmentInventory(equipmentIds: string[], inventoryResult: string, remark?: string) {
return http.post(`${BASE_URL}/inventory/batch`, null, {
params: { equipmentIds, inventoryResult, remark }
})
}

View File

@ -1282,6 +1282,16 @@ export const systemRoutes: RouteRecordRaw[] = [
hidden: false,
},
},
{
path: '/system-resource/device-management/inventory',
name: 'DeviceInventory',
component: () => import('@/views/system-resource/device-management/inventory.vue'),
meta: {
title: '设备盘库',
icon: 'inbox',
hidden: false,
},
},
{
path: '/system-resource/device-management/online',
name: 'SystemResourceDeviceOnline',

View File

@ -226,6 +226,20 @@ const storeSetup = () => {
{
id: 2013,
parentId: 2010,
title: '设备盘库',
type: 2,
path: '/asset-management/device-management/inventory',
name: 'DeviceInventory',
component: 'system-resource/device-management/inventory',
icon: 'inbox',
isExternal: false,
isCache: false,
isHidden: false,
sort: 3,
},
{
id: 2014,
parentId: 2010,
title: '审批台',
type: 2,
path: '/asset-management/device-management/approval',
@ -235,10 +249,10 @@ const storeSetup = () => {
isExternal: false,
isCache: false,
isHidden: false,
sort: 3,
sort: 4,
},
{
id: 2014,
id: 2015,
parentId: 2010,
title: '在线管理',
type: 1,
@ -250,11 +264,11 @@ const storeSetup = () => {
isExternal: false,
isCache: false,
isHidden: false,
sort: 4,
sort: 5,
children: [
{
id: 20141,
parentId: 2014,
id: 20151,
parentId: 2015,
title: '无人机',
type: 2,
path: '/asset-management/device-management/online/drone',
@ -267,8 +281,8 @@ const storeSetup = () => {
sort: 1,
},
{
id: 20142,
parentId: 2014,
id: 20152,
parentId: 2015,
title: '机巢',
type: 2,
path: '/asset-management/device-management/online/nest',
@ -281,8 +295,8 @@ const storeSetup = () => {
sort: 2,
},
{
id: 20143,
parentId: 2014,
id: 20153,
parentId: 2015,
title: '其他智能终端',
type: 2,
path: '/asset-management/device-management/online/smart-terminal',
@ -297,7 +311,7 @@ const storeSetup = () => {
],
},
{
id: 2015,
id: 2016,
parentId: 2010,
title: '设备详情',
type: 2,
@ -308,7 +322,7 @@ const storeSetup = () => {
isExternal: false,
isCache: false,
isHidden: true,
sort: 5,
sort: 6,
},
],
},

View File

@ -7,6 +7,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
CircularProgress: typeof import('./../components/CircularProgress/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}

View File

@ -0,0 +1,201 @@
<template>
<a-modal
:visible="visible"
title="设备类型管理"
width="600px"
:confirm-loading="loading"
@cancel="handleCancel"
@ok="handleSubmit"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
layout="vertical"
>
<a-form-item label="类型名称" field="typeName" required>
<a-input
v-model="formData.typeName"
placeholder="请输入设备类型名称"
show-word-limit
:max-length="50"
/>
</a-form-item>
<a-form-item label="类型编码" field="typeCode" required>
<a-input
v-model="formData.typeCode"
placeholder="请输入设备类型编码"
show-word-limit
:max-length="20"
/>
</a-form-item>
<a-form-item label="类型描述" field="description">
<a-textarea
v-model="formData.description"
placeholder="请输入设备类型描述"
:rows="4"
show-word-limit
:max-length="200"
/>
</a-form-item>
<a-form-item label="排序" field="sort">
<a-input-number
v-model="formData.sort"
placeholder="请输入排序值"
:min="0"
:max="999"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="状态" field="status">
<a-select
v-model="formData.status"
placeholder="请选择状态"
style="width: 100%"
>
<a-option value="1">启用</a-option>
<a-option value="0">禁用</a-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { FormInstance } from '@arco-design/web-vue'
interface Props {
visible: boolean
typeData?: any
mode: 'add' | 'edit'
}
interface FormDataType {
typeName: string
typeCode: string
description: string
sort: number
status: string
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
typeData: null,
mode: 'add',
})
const emit = defineEmits<{
'update:visible': [value: boolean]
'success': []
}>()
const formRef = ref<FormInstance>()
const loading = ref(false)
//
const formData = reactive<FormDataType>({
typeName: '',
typeCode: '',
description: '',
sort: 0,
status: '1'
})
//
const rules = {
typeName: [
{ required: true, message: '请输入设备类型名称' },
{ min: 2, max: 50, message: '类型名称长度应在2-50个字符之间' }
],
typeCode: [
{ required: true, message: '请输入设备类型编码' },
{ min: 2, max: 20, message: '类型编码长度应在2-20个字符之间' },
{ pattern: /^[A-Z_]+$/, message: '类型编码只能包含大写字母和下划线' }
],
description: [
{ max: 200, message: '类型描述不能超过200个字符' }
],
sort: [
{ type: 'number', min: 0, max: 999, message: '排序值应在0-999之间' }
]
}
//
watch(() => props.typeData, (newData) => {
if (newData) {
Object.assign(formData, {
typeName: newData.typeName || '',
typeCode: newData.typeCode || '',
description: newData.description || '',
sort: newData.sort || 0,
status: newData.status || '1'
})
}
}, { immediate: true })
//
watch(() => props.visible, (visible) => {
if (visible && props.mode === 'add') {
//
Object.assign(formData, {
typeName: '',
typeCode: '',
description: '',
sort: 0,
status: '1'
})
formRef.value?.clearValidate()
}
})
//
const handleSubmit = async () => {
try {
await formRef.value?.validate()
loading.value = true
// API
//
await new Promise(resolve => setTimeout(resolve, 1000))
Message.success(props.mode === 'add' ? '设备类型添加成功' : '设备类型更新成功')
emit('success')
emit('update:visible', false)
} catch (error) {
console.error('保存失败:', error)
Message.error('保存失败,请检查表单信息')
} finally {
loading.value = false
}
}
//
const handleCancel = () => {
emit('update:visible', false)
}
</script>
<style scoped lang="scss">
.arco-form-item {
margin-bottom: 20px;
}
.arco-input,
.arco-textarea,
.arco-select,
.arco-input-number {
border-radius: 6px;
}
.arco-textarea {
resize: vertical;
}
</style>

View File

@ -19,6 +19,12 @@
@search="handleSearch"
@reset="handleReset"
/>
<a-button type="outline" @click="handleAddEquipmentType" size="large">
<template #icon>
<IconTag />
</template>
添加设备类型
</a-button>
<a-button type="primary" @click="handleAdd" size="large">
<template #icon>
<IconPlus />
@ -235,6 +241,14 @@
:mode="modalMode"
@success="handleModalSuccess"
/>
<!-- 设备类型管理弹窗 -->
<EquipmentTypeModal
v-model:visible="equipmentTypeModalVisible"
:type-data="currentEquipmentType"
:mode="equipmentTypeModalMode"
@success="handleEquipmentTypeModalSuccess"
/>
</div>
</template>
@ -250,9 +264,11 @@ import {
IconPlus,
IconRefresh,
IconSearch,
IconTag,
} from '@arco-design/web-vue/es/icon'
import message from '@arco-design/web-vue/es/message'
import DeviceModal from './components/DeviceModal.vue'
import EquipmentTypeModal from './components/EquipmentTypeModal.vue'
import EquipmentSearch from './components/EquipmentSearch.vue'
import router from '@/router'
import { EquipmentAPI } from '@/apis'
@ -287,6 +303,11 @@ const modalVisible = ref(false)
const currentEquipment = ref<EquipmentResp | null>(null)
const modalMode = ref<'add' | 'edit' | 'view'>('add')
//
const equipmentTypeModalVisible = ref(false)
const currentEquipmentType = ref<any>(null)
const equipmentTypeModalMode = ref<'add' | 'edit'>('add')
//
const columns = [
{
@ -913,6 +934,19 @@ const handleModalSuccess = () => {
loadData()
}
//
const handleAddEquipmentType = () => {
equipmentTypeModalMode.value = 'add'
currentEquipmentType.value = null
equipmentTypeModalVisible.value = true
}
//
const handleEquipmentTypeModalSuccess = () => {
equipmentTypeModalVisible.value = false
loadData()
}
//
watch(tableData, (_newData) => {
//

View File

@ -0,0 +1,330 @@
<template>
<div class="equipment-inventory">
<a-card title="设备盘库管理" :bordered="false">
<template #extra>
<a-button type="primary" @click="handleBatchInventory">
<template #icon>
<IconPlus />
</template>
批量盘库
</a-button>
</template>
<!-- 搜索区域 -->
<div class="search-container">
<a-form layout="inline" :model="searchForm" class="search-form">
<a-form-item label="设备名称">
<a-input
v-model="searchForm.equipmentName"
placeholder="请输入设备名称"
allow-clear
style="width: 200px"
@input="debouncedSearch"
/>
</a-form-item>
<a-form-item label="资产编号">
<a-input
v-model="searchForm.assetCode"
placeholder="请输入资产编号"
allow-clear
style="width: 200px"
@input="debouncedSearch"
/>
</a-form-item>
<a-form-item label="设备类型">
<a-select
v-model="searchForm.equipmentType"
placeholder="请选择类型"
allow-clear
style="width: 150px"
@change="debouncedSearch"
>
<a-option value="">全部</a-option>
<a-option value="COMPUTER">计算机设备</a-option>
<a-option value="NETWORK">网络设备</a-option>
<a-option value="STORAGE">存储设备</a-option>
<a-option value="SECURITY">安防设备</a-option>
<a-option value="OTHER">其他设备</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="search">
<template #icon><IconSearch /></template>
搜索
</a-button>
<a-button @click="reset">
<template #icon><IconRefresh /></template>
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
<a-table
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
:row-selection="rowSelection"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
>
<template #toolbar-left>
<span class="search-result-info">
共找到 <strong>{{ pagination.total }}</strong> 条记录
</span>
</template>
<template #equipmentStatus="{ record }">
<a-tag :color="getStatusColor(record.equipmentStatus)">
{{ getStatusText(record.equipmentStatus) }}
</a-tag>
</template>
<template #operations="{ record }">
<a-space>
<a-button type="text" size="small" @click="handleView(record)">
查看详情
</a-button>
<a-button type="text" size="small" @click="handleInventory(record)">
执行盘库
</a-button>
</a-space>
</template>
</a-table>
</a-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { Message } from '@arco-design/web-vue'
import { IconSearch, IconRefresh, IconPlus } from '@arco-design/web-vue/es/icon'
import { pageEquipmentInventory } from '@/apis/equipment'
import type { EquipmentResp } from '@/types/equipment.d'
//
const debounce = (func: Function, delay: number) => {
let timeoutId: NodeJS.Timeout
return (...args: any[]) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => func.apply(null, args), delay)
}
}
defineOptions({ name: 'EquipmentInventory' })
//
const columns = [
{ title: '设备名称', dataIndex: 'equipmentName', key: 'equipmentName', width: 180 },
{ title: '资产编号', dataIndex: 'assetCode', key: 'assetCode', width: 150 },
{ title: '设备类型', dataIndex: 'equipmentType', key: 'equipmentType', width: 120 },
{ title: '品牌', dataIndex: 'brand', key: 'brand', width: 100 },
{ title: '型号', dataIndex: 'equipmentModel', key: 'equipmentModel', width: 120 },
{ title: '设备状态', dataIndex: 'equipmentStatus', key: 'equipmentStatus', slotName: 'equipmentStatus', width: 100 },
{ title: '负责人', dataIndex: 'responsiblePerson', key: 'responsiblePerson', width: 100 },
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 180 },
{ title: '操作', key: 'operations', slotName: 'operations', width: 200 }
]
//
const searchForm = reactive({
equipmentName: '',
assetCode: '',
equipmentType: ''
})
//
const tableData = ref<EquipmentResp[]>([])
const loading = ref(false)
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showTotal: true,
showJumper: true,
showPageSize: true
})
//
const selectedRowKeys = ref<string[]>([])
const rowSelection = computed(() => ({
selectedRowKeys: selectedRowKeys.value,
onChange: (keys: string[]) => {
selectedRowKeys.value = keys
}
}))
//
const getStatusColor = (status: string) => {
const colors: Record<string, string> = {
'IDLE': 'blue',
'IN_USE': 'green',
'MAINTENANCE': 'orange',
'REPAIR': 'red',
'SCRAPPED': 'gray'
}
return colors[status] || 'blue'
}
//
const getStatusText = (status: string) => {
const texts: Record<string, string> = {
'IDLE': '空闲中',
'IN_USE': '使用中',
'MAINTENANCE': '保养中',
'REPAIR': '维修中',
'SCRAPPED': '已报废'
}
return texts[status] || '未知'
}
//
const getTableData = async () => {
loading.value = true
try {
const response = await pageEquipmentInventory({
pageNum: pagination.current,
pageSize: pagination.pageSize,
equipmentName: searchForm.equipmentName || undefined,
assetCode: searchForm.assetCode || undefined,
equipmentType: searchForm.equipmentType || undefined
})
if (response.status === 200) {
//
if (response.data && Array.isArray(response.data)) {
//
tableData.value = response.data
pagination.total = response.data.length
} else if (response.data && typeof response.data === 'object' && 'records' in response.data) {
//
const pageData = response.data as any
tableData.value = pageData.records || []
pagination.total = pageData.total || pageData.records?.length || 0
pagination.current = pageData.current || 1
} else {
tableData.value = []
pagination.total = 0
}
} else {
Message.error('获取数据失败')
}
} catch (error) {
console.error('获取设备盘库列表失败:', error)
Message.error('获取数据失败')
} finally {
loading.value = false
}
}
//
const debouncedSearch = debounce(() => {
pagination.current = 1
getTableData()
}, 300)
//
const search = () => {
pagination.current = 1
getTableData()
}
//
const reset = () => {
Object.assign(searchForm, {
equipmentName: '',
assetCode: '',
equipmentType: ''
})
pagination.current = 1
getTableData()
}
//
const handleView = (record: EquipmentResp) => {
console.log('查看设备详情:', record)
}
//
const handleInventory = (record: EquipmentResp) => {
console.log('执行设备盘库:', record)
}
//
const handleBatchInventory = () => {
if (selectedRowKeys.value.length === 0) {
Message.warning('请先选择要盘库的设备')
return
}
console.log('批量盘库设备:', selectedRowKeys.value)
}
//
const handlePageChange = (page: number) => {
pagination.current = page
getTableData()
}
const handlePageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
pagination.current = 1
getTableData()
}
onMounted(() => {
getTableData()
})
</script>
<style scoped lang="scss">
.equipment-inventory {
.arco-card {
margin-bottom: 16px;
}
}
.search-container {
padding: 16px;
background: var(--color-bg-2);
border-radius: 6px;
margin-bottom: 16px;
.search-form {
.arco-form-item {
margin-bottom: 0;
margin-right: 16px;
.arco-form-item-label {
font-weight: 500;
color: var(--color-text-1);
margin-right: 8px;
}
}
.arco-input,
.arco-select {
border-radius: 4px;
}
.arco-btn {
border-radius: 4px;
}
}
}
.search-result-info {
color: var(--color-text-2);
font-size: 14px;
strong {
color: var(--color-text-1);
font-weight: 600;
}
}
</style>