feat: add equipment health and inspection template frontend pages
This commit is contained in:
parent
5c7728e3db
commit
7b3194219b
|
|
@ -822,6 +822,16 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/echarts": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.0.0.tgz",
|
||||
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0",
|
||||
"zrender": "6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
|
||||
|
|
@ -1799,6 +1809,12 @@
|
|||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-detect": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/type-detect/-/type-detect-4.1.0.tgz",
|
||||
|
|
@ -2152,6 +2168,15 @@
|
|||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zrender": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-6.0.0.tgz",
|
||||
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
"dependencies": {
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.7.0",
|
||||
"echarts": "^6.0.0",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.3.0"
|
||||
|
|
@ -1552,6 +1553,16 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/echarts": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.0.0.tgz",
|
||||
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0",
|
||||
"zrender": "6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
|
||||
|
|
@ -2529,6 +2540,12 @@
|
|||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-detect": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/type-detect/-/type-detect-4.1.0.tgz",
|
||||
|
|
@ -2882,6 +2899,15 @@
|
|||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zrender": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-6.0.0.tgz",
|
||||
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"dependencies": {
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.7.0",
|
||||
"echarts": "^6.0.0",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.3.0"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// ==================== 设备健康度类型 ====================
|
||||
|
||||
export interface EquipmentHealth {
|
||||
equipmentId: string
|
||||
equipmentName: string
|
||||
healthScore: number
|
||||
healthLevel: string
|
||||
lastCheckTime: string
|
||||
nextCheckTime?: string
|
||||
riskLevel: string
|
||||
mainRiskFactors: string[]
|
||||
maintenanceSuggestions: string[]
|
||||
}
|
||||
|
||||
export interface HealthHistory {
|
||||
id: string
|
||||
equipmentId: string
|
||||
healthScore: number
|
||||
healthLevel: string
|
||||
recordTime: string
|
||||
factors: Record<string, number>
|
||||
}
|
||||
|
||||
export interface EquipmentFailure {
|
||||
id: string
|
||||
equipmentId: string
|
||||
equipmentName: string
|
||||
failureTime: string
|
||||
failureType: string
|
||||
failureLevel: string
|
||||
description: string
|
||||
repairTime?: string
|
||||
repairMethod?: string
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface MTBFData {
|
||||
equipmentId: string
|
||||
equipmentName: string
|
||||
mtbfDays: number
|
||||
totalFailures: number
|
||||
totalOperatingDays: number
|
||||
lastFailureTime?: string
|
||||
}
|
||||
|
||||
export interface MTTRData {
|
||||
equipmentId: string
|
||||
equipmentName: string
|
||||
mttrHours: number
|
||||
totalRepairTime: number
|
||||
totalRepairs: number
|
||||
lastRepairTime?: string
|
||||
}
|
||||
|
||||
export interface HealthTrendData {
|
||||
date: string
|
||||
score: number
|
||||
level: string
|
||||
}
|
||||
|
||||
// ==================== 设备健康 API ====================
|
||||
|
||||
// 获取设备健康度
|
||||
export function getEquipmentHealth(equipmentId: string) {
|
||||
return request.get<EquipmentHealth>(`/api/v1/ops/equipment-health/${equipmentId}`)
|
||||
}
|
||||
|
||||
// 获取健康度历史
|
||||
export function getHealthHistory(equipmentId: string, days: number = 30) {
|
||||
return request.get<HealthHistory[]>(`/api/v1/ops/equipment-health/${equipmentId}/history`, {
|
||||
params: { days }
|
||||
})
|
||||
}
|
||||
|
||||
// 计算设备健康度
|
||||
export function calculateHealth(equipmentId: string) {
|
||||
return request.post<EquipmentHealth>(`/api/v1/ops/equipment-health/calculate`, { equipmentId })
|
||||
}
|
||||
|
||||
// 获取故障历史列表
|
||||
export function getFailureHistory(equipmentId: string) {
|
||||
return request.get<EquipmentFailure[]>(`/api/v1/ops/equipment-failure-history/${equipmentId}`)
|
||||
}
|
||||
|
||||
// 记录故障
|
||||
export function recordFailure(data: {
|
||||
equipmentId: string
|
||||
failureTime: string
|
||||
failureType: string
|
||||
failureLevel: string
|
||||
description: string
|
||||
}) {
|
||||
return request.post('/api/v1/ops/equipment-failure-history', data)
|
||||
}
|
||||
|
||||
// 获取 MTBF
|
||||
export function getEquipmentMTBF(equipmentId: string) {
|
||||
return request.get<MTBFData>(`/api/v1/ops/equipment-mtbf/${equipmentId}`)
|
||||
}
|
||||
|
||||
// 获取 MTTR
|
||||
export function getEquipmentMTTR(equipmentId: string) {
|
||||
return request.get<MTTRData>(`/api/v1/ops/equipment-mttr/${equipmentId}`)
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// ==================== 点检模板类型 ====================
|
||||
|
||||
export interface InspectionItem {
|
||||
id: string
|
||||
itemName: string
|
||||
checkMethod: string
|
||||
checkStandard: string
|
||||
isRequired: boolean
|
||||
remarks?: string
|
||||
}
|
||||
|
||||
export interface InspectionTemplate {
|
||||
id: string
|
||||
name: string
|
||||
equipmentType: string
|
||||
projectId: string
|
||||
projectName: string
|
||||
inspectionItems: InspectionItem[]
|
||||
enabled: boolean
|
||||
createdBy?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export interface TemplateFormData {
|
||||
id?: string
|
||||
name: string
|
||||
equipmentType: string
|
||||
projectId: string
|
||||
inspectionItems: InspectionItem[]
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
// ==================== 点检模板 API ====================
|
||||
|
||||
// 获取模板列表
|
||||
export function getInspectionTemplates(projectId: string) {
|
||||
return request.get<InspectionTemplate[]>('/api/v1/ops/inspection-templates', {
|
||||
params: { projectId }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取模板详情
|
||||
export function getInspectionTemplateDetail(id: string) {
|
||||
return request.get<InspectionTemplate>(`/api/v1/ops/inspection-templates/${id}`)
|
||||
}
|
||||
|
||||
// 创建模板
|
||||
export function createInspectionTemplate(data: TemplateFormData) {
|
||||
return request.post<InspectionTemplate>('/api/v1/ops/inspection-templates', data)
|
||||
}
|
||||
|
||||
// 更新模板
|
||||
export function updateInspectionTemplate(id: string, data: TemplateFormData) {
|
||||
return request.put<InspectionTemplate>(`/api/v1/ops/inspection-templates/${id}`, data)
|
||||
}
|
||||
|
||||
// 复制模板
|
||||
export function copyInspectionTemplate(id: string, targetProjectId?: string) {
|
||||
return request.post<InspectionTemplate>(`/api/v1/ops/inspection-templates/${id}/copy`, {
|
||||
targetProjectId
|
||||
})
|
||||
}
|
||||
|
||||
// 按设备类型获取模板
|
||||
export function getTemplatesByEquipmentType(equipmentType: string) {
|
||||
return request.get<InspectionTemplate[]>(`/api/v1/ops/inspection-templates/by-type/${equipmentType}`)
|
||||
}
|
||||
|
||||
// 删除模板
|
||||
export function deleteInspectionTemplate(id: string) {
|
||||
return request.delete(`/api/v1/ops/inspection-templates/${id}`)
|
||||
}
|
||||
|
|
@ -31,7 +31,9 @@ const handleBack = () => {
|
|||
</svg>
|
||||
</button>
|
||||
<h2 v-if="title" class="page-title">{{ title }}</h2>
|
||||
<slot name="title"></slot>
|
||||
<template v-else>
|
||||
<slot name="title"></slot>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="actions && actions.length > 0" class="page-header-actions">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -0,0 +1,309 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { Tree, Button, Space, Card, Descriptions, DescriptionsItem, Table, Tag, message, Drawer, Form, Input, Select, Popconfirm, Empty } from 'ant-design-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||
import type { DataNode } from 'ant-design-vue/es/tree'
|
||||
import {
|
||||
getSpaceTree,
|
||||
getSpaceNode,
|
||||
createSpaceNode,
|
||||
updateSpaceNode,
|
||||
deleteSpaceNode
|
||||
} from '@/api/space'
|
||||
import type { SpaceNode, SpaceNodeCreateForm, SpaceNodeType, SpaceNodeCategory } from '@/types/space'
|
||||
import { SpaceNodeTypeMap, SpaceNodeCategoryMap } from '@/types/space'
|
||||
|
||||
interface Props {
|
||||
projectId?: string
|
||||
mode?: 'view' | 'edit'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
mode: 'view'
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const treeLoading = ref(false)
|
||||
const selectedNode = ref<SpaceNode | null>(null)
|
||||
const treeData = ref<SpaceNodeTree[]>([])
|
||||
|
||||
interface SpaceNodeTree extends SpaceNode {
|
||||
children?: SpaceNodeTree[]
|
||||
}
|
||||
|
||||
const drawerVisible = ref(false)
|
||||
const drawerTitle = ref('')
|
||||
const submitting = ref(false)
|
||||
const formState = ref<SpaceNodeCreateForm>({
|
||||
projectId: props.projectId,
|
||||
name: '',
|
||||
nodeCategory: 'BUILDING',
|
||||
nodeType: 'BUILDING',
|
||||
parentId: undefined,
|
||||
sortOrder: 0,
|
||||
status: 'ACTIVE'
|
||||
})
|
||||
|
||||
const expandedKeys = ref<string[]>([])
|
||||
const selectedKeys = ref<string[]>([])
|
||||
|
||||
const categoryOptions = Object.entries(SpaceNodeCategoryMap).map(([value, { label }]) => ({ value, label }))
|
||||
const typeOptions = computed(() => {
|
||||
const category = formState.value.nodeCategory
|
||||
if (category === 'BUILDING') {
|
||||
return Object.entries(SpaceNodeTypeMap).filter(([key]) => ['BUILDING', 'UNIT', 'FLOOR', 'ROOM'].includes(key)).map(([value, { label }]) => ({ value, label }))
|
||||
} else if (category === 'FACILITY') {
|
||||
return Object.entries(SpaceNodeTypeMap).filter(([key]) => ['EQUIPMENT_ROOM', 'ELECTRIC_ROOM', 'WATER_ROOM', 'PARKING_LOT', 'STORAGE'].includes(key)).map(([value, { label }]) => ({ value, label }))
|
||||
} else if (category === 'OUTDOOR') {
|
||||
return Object.entries(SpaceNodeTypeMap).filter(([key]) => ['GREEN_AREA', 'ROAD', 'PARKING_SPACE'].includes(key)).map(([value, { label }]) => ({ value, label }))
|
||||
}
|
||||
return Object.entries(SpaceNodeTypeMap).map(([value, { label }]) => ({ value, label }))
|
||||
})
|
||||
|
||||
const fetchTree = async () => {
|
||||
if (!props.projectId) return
|
||||
treeLoading.value = true
|
||||
try {
|
||||
const res = await getSpaceTree(props.projectId)
|
||||
treeData.value = res.data.data || []
|
||||
if (treeData.value.length > 0 && expandedKeys.value.length === 0) {
|
||||
expandedKeys.value = [treeData.value[0].id]
|
||||
selectedKeys.value = [treeData.value[0].id]
|
||||
selectedNode.value = treeData.value[0]
|
||||
}
|
||||
} catch {
|
||||
message.error('获取空间树失败')
|
||||
} finally {
|
||||
treeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const transformToTreeData = (nodes: SpaceNodeTree[]): DataNode[] => {
|
||||
return nodes.map(node => ({
|
||||
key: node.id,
|
||||
title: node.name,
|
||||
isLeaf: !node.children || node.children.length === 0,
|
||||
children: node.children ? transformToTreeData(node.children) : undefined
|
||||
}))
|
||||
}
|
||||
|
||||
const handleTreeSelect = async (keys: string[]) => {
|
||||
if (keys.length === 0) return
|
||||
const nodeId = keys[0]
|
||||
selectedKeys.value = [nodeId]
|
||||
try {
|
||||
const res = await getSpaceNode(nodeId)
|
||||
selectedNode.value = res.data.data
|
||||
} catch {
|
||||
message.error('获取节点详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleTreeExpand = (keys: string[]) => {
|
||||
expandedKeys.value = keys
|
||||
}
|
||||
|
||||
const handleAdd = (parentId?: string) => {
|
||||
drawerTitle.value = parentId ? '新增子节点' : '新增根节点'
|
||||
formState.value = {
|
||||
projectId: props.projectId,
|
||||
name: '',
|
||||
nodeCategory: 'BUILDING',
|
||||
nodeType: 'BUILDING',
|
||||
parentId: parentId,
|
||||
sortOrder: 0,
|
||||
status: 'ACTIVE'
|
||||
}
|
||||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
if (!selectedNode.value) return
|
||||
drawerTitle.value = '编辑节点'
|
||||
formState.value = {
|
||||
projectId: props.projectId,
|
||||
name: selectedNode.value.name,
|
||||
fullName: selectedNode.value.fullName,
|
||||
shortName: selectedNode.value.shortName,
|
||||
nodeCategory: selectedNode.value.nodeCategory,
|
||||
nodeType: selectedNode.value.nodeType,
|
||||
parentId: selectedNode.value.parentId,
|
||||
sortOrder: selectedNode.value.sortOrder || 0,
|
||||
status: selectedNode.value.status || 'ACTIVE'
|
||||
}
|
||||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!selectedNode.value) return
|
||||
try {
|
||||
await deleteSpaceNode(selectedNode.value.id)
|
||||
message.success('删除成功')
|
||||
selectedNode.value = null
|
||||
fetchTree()
|
||||
} catch {
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
submitting.value = true
|
||||
if (selectedNode.value && drawerTitle.value === '编辑节点') {
|
||||
await updateSpaceNode(selectedNode.value.id, formState.value)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await createSpaceNode(formState.value)
|
||||
message.success('创建成功')
|
||||
}
|
||||
drawerVisible.value = false
|
||||
fetchTree()
|
||||
} catch {
|
||||
message.error('操作失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.projectId, () => {
|
||||
if (props.projectId) {
|
||||
fetchTree()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
const columns: ColumnsType = [
|
||||
{ title: '名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '类型', dataIndex: 'nodeType', key: 'nodeType' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-manage-container">
|
||||
<Card v-if="projectId" size="small" :bodyStyle="{ padding: '12px' }">
|
||||
<div class="space-header">
|
||||
<Space>
|
||||
<Button type="primary" size="small" @click="handleAdd()">
|
||||
<PlusOutlined /> 新增根节点
|
||||
</Button>
|
||||
<Button size="small" :disabled="!selectedNode" @click="handleAdd(selectedNode?.id)">
|
||||
<PlusOutlined /> 新增子节点
|
||||
</Button>
|
||||
<Button size="small" :disabled="!selectedNode" @click="handleEdit">
|
||||
<EditOutlined /> 编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
v-if="selectedNode"
|
||||
title="确认删除?"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="handleDelete"
|
||||
>
|
||||
<Button size="small" danger :disabled="!selectedNode">
|
||||
<DeleteOutlined /> 删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</div>
|
||||
<div class="space-content">
|
||||
<div class="space-tree">
|
||||
<Tree
|
||||
v-if="treeData.length > 0"
|
||||
:treeData="transformToTreeData(treeData)"
|
||||
:expandedKeys="expandedKeys"
|
||||
:selectedKeys="selectedKeys"
|
||||
:loading="treeLoading"
|
||||
:show-icon="true"
|
||||
@select="handleTreeSelect"
|
||||
@expand="handleTreeExpand"
|
||||
/>
|
||||
<Empty v-else description="暂无空间数据" />
|
||||
</div>
|
||||
<div class="space-detail">
|
||||
<template v-if="selectedNode">
|
||||
<Descriptions :column="1" size="small" bordered>
|
||||
<DescriptionsItem label="名称">{{ selectedNode.name }}</DescriptionsItem>
|
||||
<DescriptionsItem label="类型">{{ SpaceNodeTypeMap[selectedNode.nodeType as SpaceNodeType]?.label || selectedNode.nodeType }}</DescriptionsItem>
|
||||
<DescriptionsItem label="状态">
|
||||
<Tag :color="selectedNode.status === 'ACTIVE' ? 'green' : 'red'">
|
||||
{{ selectedNode.status === 'ACTIVE' ? '正常' : '禁用' }}
|
||||
</Tag>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="全称">{{ selectedNode.fullName || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="简称">{{ selectedNode.shortName || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="建筑面积">{{ selectedNode.buildingArea || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="地址">{{ selectedNode.address || '-' }}</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</template>
|
||||
<Empty v-else description="请选择空间节点" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div v-else style="text-align: center; padding: 40px; color: #999">
|
||||
请先保存项目后再管理空间
|
||||
</div>
|
||||
|
||||
<Drawer
|
||||
v-model:open="drawerVisible"
|
||||
:title="drawerTitle"
|
||||
width="400px"
|
||||
@close="drawerVisible = false"
|
||||
>
|
||||
<Form :model="formState" layout="vertical">
|
||||
<Form.Item label="节点大类" name="nodeCategory">
|
||||
<Select v-model:value="formState.nodeCategory" :options="categoryOptions" />
|
||||
</Form.Item>
|
||||
<Form.Item label="节点类型" name="nodeType">
|
||||
<Select v-model:value="formState.nodeType" :options="typeOptions" />
|
||||
</Form.Item>
|
||||
<Form.Item label="名称" name="name">
|
||||
<Input v-model:value="formState.name" placeholder="请输入名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="全称" name="fullName">
|
||||
<Input v-model:value="formState.fullName" placeholder="请输入全称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简称" name="shortName">
|
||||
<Input v-model:value="formState.shortName" placeholder="请输入简称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" name="status">
|
||||
<Select v-model:value="formState.status" :options="[{ value: 'ACTIVE', label: '正常' }, { value: 'DISABLED', label: '禁用' }]" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="drawerVisible = false">取消</Button>
|
||||
<Button type="primary" :loading="submitting" @click="handleSubmit">确定</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</Drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.space-manage-container {
|
||||
min-height: 400px;
|
||||
}
|
||||
.space-header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.space-content {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
min-height: 300px;
|
||||
}
|
||||
.space-tree {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
border: 1px solid #f0f0f0;
|
||||
padding: 8px;
|
||||
overflow: auto;
|
||||
}
|
||||
.space-detail {
|
||||
flex: 1;
|
||||
border: 1px solid #f0f0f0;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -63,3 +63,6 @@ export { default as PasswordItem } from './PasswordItem/index.vue'
|
|||
// 业务组件 - 详情
|
||||
export { default as DescriptionList } from './DescriptionList/index.vue'
|
||||
export { default as ProfileCard } from './ProfileCard/index.vue'
|
||||
|
||||
// 业务组件 - 空间
|
||||
export { default as SpaceTree } from './SpaceTree/index.vue'
|
||||
|
|
|
|||
|
|
@ -81,6 +81,18 @@ const router = createRouter({
|
|||
component: () => import('@/views/equipment/EquipmentDetail.vue'),
|
||||
meta: { title: '设备详情' }
|
||||
},
|
||||
{
|
||||
path: 'equipment/health',
|
||||
name: 'EquipmentHealth',
|
||||
component: () => import('@/views/equipment/EquipmentHealth.vue'),
|
||||
meta: { title: '设备健康预测' }
|
||||
},
|
||||
{
|
||||
path: 'inspection/templates',
|
||||
name: 'InspectionTemplates',
|
||||
component: () => import('@/views/inspection/TemplateList.vue'),
|
||||
meta: { title: '点检模板' }
|
||||
},
|
||||
{
|
||||
path: 'maintenance/plans',
|
||||
name: 'MaintenancePlans',
|
||||
|
|
|
|||
|
|
@ -39,7 +39,9 @@ const menuItems: MenuProps['items'] = [
|
|||
type: 'group',
|
||||
children: [
|
||||
{ key: '/project/list', icon: () => h(BuildOutlined), label: '项目管理' },
|
||||
{ key: '/equipment/list', icon: () => h(ToolOutlined), label: '设备管理' }
|
||||
{ key: '/equipment/list', icon: () => h(ToolOutlined), label: '设备管理' },
|
||||
{ key: '/equipment/health', icon: () => h(ToolOutlined), label: '设备健康预测' },
|
||||
{ key: '/inspection/templates', icon: () => h(ToolOutlined), label: '点检模板' }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,441 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Select, Button, Space, message, Card, Tag, Row, Col, Statistic, Spin } from 'ant-design-vue'
|
||||
import { SearchOutlined, ReloadOutlined, SyncOutlined } from '@ant-design/icons-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import * as echarts from 'echarts'
|
||||
import {
|
||||
getEquipmentHealth,
|
||||
getHealthHistory,
|
||||
getFailureHistory,
|
||||
getEquipmentMTBF,
|
||||
getEquipmentMTTR,
|
||||
calculateHealth,
|
||||
type EquipmentHealth,
|
||||
type HealthHistory,
|
||||
type EquipmentFailure,
|
||||
type MTBFData,
|
||||
type MTTRData
|
||||
} from '@/api/equipment-health'
|
||||
import { getEquipmentList, type Equipment } from '@/api/equipment'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const calculating = ref(false)
|
||||
const selectedEquipmentId = ref<string>('')
|
||||
const projectId = ref<string>('')
|
||||
|
||||
// 数据
|
||||
const equipmentOptions = ref<{ value: string; label: string }[]>([])
|
||||
const healthData = ref<EquipmentHealth | null>(null)
|
||||
const healthHistory = ref<HealthHistory[]>([])
|
||||
const failureHistory = ref<EquipmentFailure[]>([])
|
||||
const mtbfData = ref<MTBFData | null>(null)
|
||||
const mttrData = ref<MTTRData | null>(null)
|
||||
|
||||
// 图表实例
|
||||
let healthChart: echarts.ECharts | null = null
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
const chartDom = document.getElementById('health-chart')
|
||||
if (chartDom) {
|
||||
healthChart = echarts.init(chartDom)
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染健康度趋势图
|
||||
const renderHealthChart = () => {
|
||||
if (!healthChart) return
|
||||
|
||||
const dates = healthHistory.value.map(h => h.recordTime.substring(0, 10))
|
||||
const scores = healthHistory.value.map(h => h.healthScore)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
boundaryGap: false
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 100,
|
||||
axisLabel: {
|
||||
formatter: '{value}%'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '健康度',
|
||||
type: 'line',
|
||||
data: scores,
|
||||
smooth: true,
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(82, 196, 26, 0.4)' },
|
||||
{ offset: 1, color: 'rgba(82, 196, 26, 0.05)' }
|
||||
])
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#52c41a'
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#52c41a'
|
||||
}
|
||||
}
|
||||
],
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
}
|
||||
}
|
||||
|
||||
healthChart.setOption(option)
|
||||
}
|
||||
|
||||
// 获取设备列表
|
||||
const fetchEquipmentList = async (pId: string) => {
|
||||
if (!pId) return
|
||||
try {
|
||||
const res = await getEquipmentList(pId)
|
||||
const data = res.data.data
|
||||
equipmentOptions.value = (data.content || []).map((item: Equipment) => ({
|
||||
value: item.id,
|
||||
label: `${item.name} (${item.code})`
|
||||
}))
|
||||
} catch {
|
||||
message.error('获取设备列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取健康数据
|
||||
const fetchHealthData = async () => {
|
||||
if (!selectedEquipmentId.value) {
|
||||
message.warning('请先选择设备')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const [healthRes, historyRes, failureRes, mtbfRes, mttrRes] = await Promise.all([
|
||||
getEquipmentHealth(selectedEquipmentId.value),
|
||||
getHealthHistory(selectedEquipmentId.value, 30),
|
||||
getFailureHistory(selectedEquipmentId.value),
|
||||
getEquipmentMTBF(selectedEquipmentId.value),
|
||||
getEquipmentMTTR(selectedEquipmentId.value)
|
||||
])
|
||||
|
||||
healthData.value = healthRes.data.data
|
||||
healthHistory.value = historyRes.data.data || []
|
||||
failureHistory.value = failureRes.data.data || []
|
||||
mtbfData.value = mtbfRes.data.data
|
||||
mttrData.value = mttrRes.data.data
|
||||
|
||||
// 渲染图表
|
||||
setTimeout(() => {
|
||||
initChart()
|
||||
renderHealthChart()
|
||||
}, 100)
|
||||
} catch {
|
||||
message.error('获取健康数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 计算健康度
|
||||
const handleCalculate = async () => {
|
||||
if (!selectedEquipmentId.value) {
|
||||
message.warning('请先选择设备')
|
||||
return
|
||||
}
|
||||
calculating.value = true
|
||||
try {
|
||||
await calculateHealth(selectedEquipmentId.value)
|
||||
message.success('健康度计算完成')
|
||||
fetchHealthData()
|
||||
} catch {
|
||||
message.error('计算健康度失败')
|
||||
} finally {
|
||||
calculating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 健康等级颜色
|
||||
const getHealthLevelColor = (level: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
'优秀': 'green',
|
||||
'良好': 'cyan',
|
||||
'一般': 'orange',
|
||||
'较差': 'red',
|
||||
'危险': 'red'
|
||||
}
|
||||
return colorMap[level] || 'default'
|
||||
}
|
||||
|
||||
// 风险等级颜色
|
||||
const getRiskLevelColor = (level: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
'低': 'green',
|
||||
'中': 'orange',
|
||||
'高': 'red'
|
||||
}
|
||||
return colorMap[level] || 'default'
|
||||
}
|
||||
|
||||
// 故障等级颜色
|
||||
const getFailureLevelColor = (level: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
'轻微': 'green',
|
||||
'中等': 'orange',
|
||||
'严重': 'red'
|
||||
}
|
||||
return colorMap[level] || 'default'
|
||||
}
|
||||
|
||||
// 故障历史列定义
|
||||
const failureColumns: ColumnsType = [
|
||||
{ title: '故障时间', dataIndex: 'failureTime', key: 'failureTime', width: 160 },
|
||||
{ title: '故障类型', dataIndex: 'failureType', key: 'failureType', width: 120 },
|
||||
{ title: '故障等级', dataIndex: 'failureLevel', key: 'failureLevel', width: 100 },
|
||||
{ title: '故障描述', dataIndex: 'description', key: 'description' },
|
||||
{ title: '维修时间', dataIndex: 'repairTime', key: 'repairTime', width: 160 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 }
|
||||
]
|
||||
|
||||
// 项目变更
|
||||
const handleProjectChange = (value: string) => {
|
||||
projectId.value = value
|
||||
selectedEquipmentId.value = ''
|
||||
equipmentOptions.value = []
|
||||
healthData.value = null
|
||||
healthHistory.value = []
|
||||
failureHistory.value = []
|
||||
mtbfData.value = null
|
||||
mttrData.value = null
|
||||
if (value) {
|
||||
fetchEquipmentList(value)
|
||||
}
|
||||
}
|
||||
|
||||
// 设备变更
|
||||
const handleEquipmentChange = (value: string) => {
|
||||
selectedEquipmentId.value = value
|
||||
}
|
||||
|
||||
// 查看设备详情
|
||||
const handleViewEquipment = () => {
|
||||
if (selectedEquipmentId.value) {
|
||||
router.push(`/equipment/detail/${selectedEquipmentId.value}`)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', () => {
|
||||
healthChart?.resize()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">设备健康预测</h2>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-bar">
|
||||
<Space>
|
||||
<Select
|
||||
v-model:value="projectId"
|
||||
placeholder="请选择项目"
|
||||
style="width: 240px"
|
||||
@change="handleProjectChange"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="selectedEquipmentId"
|
||||
placeholder="请选择设备"
|
||||
style="width: 280px"
|
||||
:disabled="!projectId"
|
||||
:options="equipmentOptions"
|
||||
@change="handleEquipmentChange"
|
||||
/>
|
||||
<Button type="primary" @click="fetchHealthData" :loading="loading">
|
||||
<SearchOutlined /> 查询
|
||||
</Button>
|
||||
<Button @click="handleCalculate" :loading="calculating">
|
||||
<SyncOutlined /> 计算健康度
|
||||
</Button>
|
||||
<Button @click="handleViewEquipment" :disabled="!selectedEquipmentId">
|
||||
查看设备详情
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Spin :spinning="loading">
|
||||
<!-- 健康度概览 -->
|
||||
<Row :gutter="16" style="margin-bottom: 16px">
|
||||
<Col :span="6">
|
||||
<Card>
|
||||
<Statistic
|
||||
title="健康度评分"
|
||||
:value="healthData?.healthScore ?? '-'"
|
||||
suffix="分"
|
||||
:value-style="{ color: healthData && healthData.healthScore >= 80 ? '#52c41a' : healthData && healthData.healthScore >= 60 ? '#faad14' : '#ff4d4f' }"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Card>
|
||||
<Statistic title="健康等级" :value="healthData?.healthLevel ?? '-'">
|
||||
<template #suffix>
|
||||
<Tag :color="getHealthLevelColor(healthData?.healthLevel || '')" style="margin-left: 8px">
|
||||
{{ healthData?.healthLevel || '-' }}
|
||||
</Tag>
|
||||
</template>
|
||||
</Statistic>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Card>
|
||||
<Statistic title="风险等级" :value="healthData?.riskLevel ?? '-'">
|
||||
<template #suffix>
|
||||
<Tag :color="getRiskLevelColor(healthData?.riskLevel || '')" style="margin-left: 8px">
|
||||
{{ healthData?.riskLevel || '-' }}
|
||||
</Tag>
|
||||
</template>
|
||||
</Statistic>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<Card>
|
||||
<Statistic
|
||||
title="最后检测"
|
||||
:value="healthData?.lastCheckTime ? healthData.lastCheckTime.substring(0, 10) : '-'"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- MTBF / MTTR 指标 -->
|
||||
<Row :gutter="16" style="margin-bottom: 16px">
|
||||
<Col :span="12">
|
||||
<Card title="MTBF (平均故障间隔时间)">
|
||||
<Row :gutter="16">
|
||||
<Col :span="8">
|
||||
<Statistic title="MTBF" :value="mtbfData?.mtbfDays ?? '-'" suffix="天" />
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Statistic title="总故障次数" :value="mtbfData?.totalFailures ?? '-'" />
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Statistic title="总运行时长" :value="mtbfData?.totalOperatingDays ?? '-'" suffix="天" />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Card title="MTTR (平均修复时间)">
|
||||
<Row :gutter="16">
|
||||
<Col :span="8">
|
||||
<Statistic title="MTTR" :value="mttrData?.mttrHours ?? '-'" suffix="小时" />
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Statistic title="总维修次数" :value="mttrData?.totalRepairs ?? '-'" />
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Statistic title="总维修时长" :value="mttrData?.totalRepairTime ?? '-'" suffix="小时" />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 健康度趋势图 -->
|
||||
<Card title="健康度趋势 (近30天)" style="margin-bottom: 16px">
|
||||
<div id="health-chart" style="width: 100%; height: 300px"></div>
|
||||
<a-empty v-if="healthHistory.length === 0 && !loading" description="暂无趋势数据" />
|
||||
</Card>
|
||||
|
||||
<!-- 风险因素和维护建议 -->
|
||||
<Row :gutter="16" style="margin-bottom: 16px" v-if="healthData">
|
||||
<Col :span="12">
|
||||
<Card title="主要风险因素">
|
||||
<div v-if="healthData.mainRiskFactors && healthData.mainRiskFactors.length > 0">
|
||||
<Tag v-for="(factor, index) in healthData.mainRiskFactors" :key="index" color="orange" style="margin-bottom: 8px">
|
||||
{{ factor }}
|
||||
</Tag>
|
||||
</div>
|
||||
<a-empty v-else description="暂无风险因素" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Card title="维护建议">
|
||||
<div v-if="healthData.maintenanceSuggestions && healthData.maintenanceSuggestions.length > 0">
|
||||
<div v-for="(suggestion, index) in healthData.maintenanceSuggestions" :key="index" style="margin-bottom: 8px">
|
||||
<span style="color: #1890ff">{{ index + 1 }}.</span> {{ suggestion }}
|
||||
</div>
|
||||
</div>
|
||||
<a-empty v-else description="暂无维护建议" />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 故障历史 -->
|
||||
<Card title="故障历史">
|
||||
<a-table
|
||||
:columns="failureColumns"
|
||||
:data-source="failureHistory"
|
||||
:row-key="(record: EquipmentFailure) => record.id"
|
||||
:pagination="{ pageSize: 5 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'failureLevel'">
|
||||
<Tag :color="getFailureLevelColor(record.failureLevel)">
|
||||
{{ record.failureLevel }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<Tag :color="record.status === '已修复' ? 'green' : 'orange'">
|
||||
{{ record.status }}
|
||||
</Tag>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-empty v-if="failureHistory.length === 0" description="暂无故障记录" />
|
||||
</Card>
|
||||
</Spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,536 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import {
|
||||
Button,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Switch,
|
||||
message,
|
||||
Popconfirm
|
||||
} from 'ant-design-vue'
|
||||
import { PlusOutlined, EditOutlined, CopyOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import {
|
||||
getInspectionTemplates,
|
||||
getInspectionTemplateDetail,
|
||||
createInspectionTemplate,
|
||||
updateInspectionTemplate,
|
||||
copyInspectionTemplate,
|
||||
deleteInspectionTemplate,
|
||||
type InspectionTemplate,
|
||||
type InspectionItem,
|
||||
type TemplateFormData
|
||||
} from '@/api/inspection-template'
|
||||
import { getProjectSelectorList } from '@/api/project'
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
const modalLoading = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const currentTemplateId = ref<string>('')
|
||||
|
||||
// 项目选项
|
||||
const projectOptions = ref<{ value: string; label: string }[]>([])
|
||||
|
||||
// 设备类型选项
|
||||
const equipmentTypeOptions = [
|
||||
{ value: '电梯', label: '电梯' },
|
||||
{ value: '消防设备', label: '消防设备' },
|
||||
{ value: '空调', label: '空调' },
|
||||
{ value: '给排水', label: '给排水' },
|
||||
{ value: '配电', label: '配电' },
|
||||
{ value: '安防', label: '安防' },
|
||||
{ value: '其他', label: '其他' }
|
||||
]
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
projectId: ''
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<InspectionTemplate[]>([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<TemplateFormData>({
|
||||
name: '',
|
||||
equipmentType: '',
|
||||
projectId: '',
|
||||
inspectionItems: [],
|
||||
enabled: true
|
||||
})
|
||||
|
||||
// 点检项目表单
|
||||
const itemFormData = reactive<Partial<InspectionItem>>({
|
||||
itemName: '',
|
||||
checkMethod: '',
|
||||
checkStandard: '',
|
||||
isRequired: true,
|
||||
remarks: ''
|
||||
})
|
||||
|
||||
// 表单验证
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入模板名称' }],
|
||||
equipmentType: [{ required: true, message: '请选择设备类型' }],
|
||||
projectId: [{ required: true, message: '请选择项目' }]
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns: ColumnsType = [
|
||||
{ title: '模板名称', dataIndex: 'name', key: 'name', width: 180 },
|
||||
{ title: '设备类型', dataIndex: 'equipmentType', key: 'equipmentType', width: 120 },
|
||||
{ title: '所属项目', dataIndex: 'projectName', key: 'projectName', width: 150 },
|
||||
{ title: '点检项目数', key: 'itemCount', width: 100 },
|
||||
{ title: '启用状态', dataIndex: 'enabled', key: 'enabled', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 160 },
|
||||
{ title: '操作', key: 'action', width: 180, fixed: 'right' as const }
|
||||
]
|
||||
|
||||
// 获取项目列表
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const res = await getProjectSelectorList()
|
||||
projectOptions.value = (res.data.data || []).map((item: any) => ({
|
||||
value: item.id,
|
||||
label: item.name
|
||||
}))
|
||||
} catch {
|
||||
message.error('获取项目列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取模板列表
|
||||
const fetchTemplateList = async () => {
|
||||
if (!queryParams.projectId) {
|
||||
message.warning('请先选择项目')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getInspectionTemplates(queryParams.projectId)
|
||||
tableData.value = res.data.data || []
|
||||
pagination.total = tableData.value.length
|
||||
} catch {
|
||||
message.error('获取模板列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchTemplateList()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
queryParams.projectId = ''
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
}
|
||||
|
||||
// 新建模板
|
||||
const handleCreate = () => {
|
||||
isEdit.value = false
|
||||
currentTemplateId.value = ''
|
||||
resetForm()
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑模板
|
||||
const handleEdit = async (record: InspectionTemplate) => {
|
||||
isEdit.value = true
|
||||
currentTemplateId.value = record.id
|
||||
modalLoading.value = true
|
||||
try {
|
||||
const res = await getInspectionTemplateDetail(record.id)
|
||||
const detail = res.data.data
|
||||
formData.name = detail.name
|
||||
formData.equipmentType = detail.equipmentType
|
||||
formData.projectId = detail.projectId
|
||||
formData.inspectionItems = detail.inspectionItems || []
|
||||
formData.enabled = detail.enabled
|
||||
modalVisible.value = true
|
||||
} catch {
|
||||
message.error('获取模板详情失败')
|
||||
} finally {
|
||||
modalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 复制模板
|
||||
const handleCopy = async (record: InspectionTemplate) => {
|
||||
try {
|
||||
await copyInspectionTemplate(record.id)
|
||||
message.success('模板复制成功')
|
||||
fetchTemplateList()
|
||||
} catch {
|
||||
message.error('模板复制失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除模板
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await deleteInspectionTemplate(id)
|
||||
message.success('模板删除成功')
|
||||
fetchTemplateList()
|
||||
} catch {
|
||||
message.error('模板删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 保存模板
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await updateInspectionTemplate(currentTemplateId.value, formData)
|
||||
message.success('模板更新成功')
|
||||
} else {
|
||||
await createInspectionTemplate(formData)
|
||||
message.success('模板创建成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
fetchTemplateList()
|
||||
} catch {
|
||||
message.error('保存模板失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.name = ''
|
||||
formData.equipmentType = ''
|
||||
formData.projectId = queryParams.projectId
|
||||
formData.inspectionItems = []
|
||||
formData.enabled = true
|
||||
itemFormData.itemName = ''
|
||||
itemFormData.checkMethod = ''
|
||||
itemFormData.checkStandard = ''
|
||||
itemFormData.isRequired = true
|
||||
itemFormData.remarks = ''
|
||||
}
|
||||
|
||||
// 添加点检项目
|
||||
const handleAddItem = () => {
|
||||
if (!itemFormData.itemName) {
|
||||
message.warning('请输入点检项目名称')
|
||||
return
|
||||
}
|
||||
if (!itemFormData.checkMethod) {
|
||||
message.warning('请输入检查方法')
|
||||
return
|
||||
}
|
||||
if (!itemFormData.checkStandard) {
|
||||
message.warning('请输入检查标准')
|
||||
return
|
||||
}
|
||||
|
||||
const newItem: InspectionItem = {
|
||||
id: Date.now().toString(),
|
||||
itemName: itemFormData.itemName!,
|
||||
checkMethod: itemFormData.checkMethod!,
|
||||
checkStandard: itemFormData.checkStandard!,
|
||||
isRequired: itemFormData.isRequired!,
|
||||
remarks: itemFormData.remarks || ''
|
||||
}
|
||||
|
||||
formData.inspectionItems.push(newItem)
|
||||
resetItemForm()
|
||||
}
|
||||
|
||||
// 删除点检项目
|
||||
const handleRemoveItem = (index: number) => {
|
||||
formData.inspectionItems.splice(index, 1)
|
||||
}
|
||||
|
||||
// 重置点检项目表单
|
||||
const resetItemForm = () => {
|
||||
itemFormData.itemName = ''
|
||||
itemFormData.checkMethod = ''
|
||||
itemFormData.checkStandard = ''
|
||||
itemFormData.isRequired = true
|
||||
itemFormData.remarks = ''
|
||||
}
|
||||
|
||||
// 项目变更
|
||||
const handleProjectChange = (value: string) => {
|
||||
queryParams.projectId = value
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
if (isEdit.value) {
|
||||
formData.projectId = value
|
||||
}
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
pagination.current = page
|
||||
pagination.pageSize = pageSize
|
||||
}
|
||||
|
||||
// 模板状态
|
||||
const getEnabledStatus = (enabled: boolean) => {
|
||||
return enabled ? { color: 'green', text: '启用' } : { color: 'default', text: '停用' }
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchProjects()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">点检模板管理</h2>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-bar">
|
||||
<Space>
|
||||
<Select
|
||||
v-model:value="queryParams.projectId"
|
||||
placeholder="请选择项目"
|
||||
style="width: 240px"
|
||||
:options="projectOptions"
|
||||
@change="handleProjectChange"
|
||||
/>
|
||||
<Button type="primary" @click="handleSearch">
|
||||
查询
|
||||
</Button>
|
||||
<Button @click="handleReset">
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 操作区 -->
|
||||
<div class="table-toolbar">
|
||||
<Button type="primary" @click="handleCreate" :disabled="!queryParams.projectId">
|
||||
<PlusOutlined /> 新建模板
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<div class="table-card">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:row-key="(record: InspectionTemplate) => record.id"
|
||||
:pagination="{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: pagination.total,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`
|
||||
}"
|
||||
@change="(pag: any) => handlePageChange(pag.current, pag.pageSize)"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'itemCount'">
|
||||
{{ record.inspectionItems?.length || 0 }} 项
|
||||
</template>
|
||||
<template v-else-if="column.key === 'enabled'">
|
||||
<Tag :color="getEnabledStatus(record.enabled).color">
|
||||
{{ getEnabledStatus(record.enabled).text }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'createdAt'">
|
||||
{{ record.createdAt ? record.createdAt.substring(0, 19) : '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button type="link" size="small" @click="handleEdit(record)">
|
||||
<EditOutlined /> 编辑
|
||||
</Button>
|
||||
<Button type="link" size="small" @click="handleCopy(record)">
|
||||
<CopyOutlined /> 复制
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定删除该模板吗?"
|
||||
@confirm="handleDelete(record.id)"
|
||||
>
|
||||
<Button type="link" size="small" danger>
|
||||
<DeleteOutlined /> 删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<a-empty v-if="tableData.length === 0 && !loading" description="请先选择项目" />
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑模态框 -->
|
||||
<Modal
|
||||
v-model:open="modalVisible"
|
||||
:title="isEdit ? '编辑模板' : '新建模板'"
|
||||
width="900px"
|
||||
:footer="null"
|
||||
@cancel="modalVisible = false"
|
||||
>
|
||||
<div class="modal-content">
|
||||
<a-form
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
@finish="handleSave"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="模板名称" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入模板名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="设备类型" name="equipmentType">
|
||||
<a-select v-model:value="formData.equipmentType" placeholder="请选择设备类型" :options="equipmentTypeOptions" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="所属项目" name="projectId">
|
||||
<a-select v-model:value="formData.projectId" placeholder="请选择项目" :options="projectOptions" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="启用状态">
|
||||
<Switch v-model:checked="formData.enabled" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 点检项目 -->
|
||||
<a-divider>点检项目</a-divider>
|
||||
|
||||
<div class="inspection-items">
|
||||
<a-table
|
||||
:columns="[
|
||||
{ title: '项目名称', dataIndex: 'itemName', key: 'itemName' },
|
||||
{ title: '检查方法', dataIndex: 'checkMethod', key: 'checkMethod' },
|
||||
{ title: '检查标准', dataIndex: 'checkStandard', key: 'checkStandard' },
|
||||
{ title: '必检', dataIndex: 'isRequired', key: 'isRequired', width: 80 },
|
||||
{ title: '操作', key: 'action', width: 80 }
|
||||
]"
|
||||
:data-source="formData.inspectionItems"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'isRequired'">
|
||||
{{ record.isRequired ? '是' : '否' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Button type="link" size="small" danger @click="handleRemoveItem(index)">
|
||||
删除
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 添加点检项目表单 -->
|
||||
<div class="add-item-form">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="6">
|
||||
<a-input v-model:value="itemFormData.itemName" placeholder="项目名称" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input v-model:value="itemFormData.checkMethod" placeholder="检查方法" />
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input v-model:value="itemFormData.checkStandard" placeholder="检查标准" />
|
||||
</a-col>
|
||||
<a-col :span="3">
|
||||
<a-switch v-model:checked="itemFormData.isRequired" checked-children="必" un-checked-children="选" />
|
||||
</a-col>
|
||||
<a-col :span="3">
|
||||
<Button type="primary" size="small" @click="handleAddItem">添加</Button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表单操作 -->
|
||||
<div class="form-actions">
|
||||
<Space>
|
||||
<Button @click="modalVisible = false">取消</Button>
|
||||
<Button type="primary" html-type="submit">保存</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</a-form>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.inspection-items {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.add-item-form {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Button, Drawer, Form, Input, Select, Space, message, Tag, Descriptions, DescriptionsItem, Divider, Card, Statistic, Row, Col, Tabs, TabPane, Table, Empty } from 'ant-design-vue'
|
||||
import { Button, Drawer, Form, Input, Select, Space, message, Tag, Descriptions, DescriptionsItem, Card, Statistic, Row, Col, Tabs, TabPane, Table, Empty, Switch } from 'ant-design-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import {
|
||||
PlusOutlined,
|
||||
|
|
@ -16,9 +16,11 @@ import {
|
|||
deleteProject,
|
||||
enableProject,
|
||||
disableProject,
|
||||
getProjectStatistics
|
||||
getProjectStatistics,
|
||||
getProjectConfig,
|
||||
updateProjectConfig
|
||||
} from '@/api/project'
|
||||
import { TableActions, Pagination, StatusTag } from '@/components'
|
||||
import { TableActions, Pagination, StatusTag, SpaceTree } from '@/components'
|
||||
import type { Project } from '@/types'
|
||||
import type { ProjectQuery, ProjectStatus, ProjectFormData, PageResponse, ProjectType } from '@/types/project'
|
||||
import { ProjectStatusMap, ProjectTypeMap } from '@/types/project'
|
||||
|
|
@ -126,6 +128,22 @@ const editFormState = ref({
|
|||
})
|
||||
const editSubmitting = ref(false)
|
||||
|
||||
// 项目配置状态
|
||||
const projectConfig = ref<any>({
|
||||
enableReservation: false,
|
||||
enableVisitor: false,
|
||||
enableComplaint: true,
|
||||
enablePayment: false,
|
||||
enableAnnouncement: true,
|
||||
enableSurvey: false,
|
||||
enableVote: false,
|
||||
enableMaintenance: true,
|
||||
enableAsset: false,
|
||||
customConfig: {}
|
||||
})
|
||||
const configLoading = ref(false)
|
||||
const configSaving = ref(false)
|
||||
|
||||
// 新增表单状态
|
||||
const createFormState = ref({
|
||||
name: '',
|
||||
|
|
@ -163,6 +181,31 @@ const handleEdit = async (record: Project) => {
|
|||
editProject.value = record
|
||||
editActiveTab.value = 'info'
|
||||
editDrawerVisible.value = true
|
||||
editFormState.value = {
|
||||
name: record.name,
|
||||
description: record.description || '',
|
||||
address: record.address || '',
|
||||
projectType: record.projectType || 'RESIDENTIAL',
|
||||
province: record.province || '',
|
||||
city: record.city || '',
|
||||
district: record.district || '',
|
||||
status: record.status || 'ACTIVE'
|
||||
}
|
||||
loadProjectConfig(record.id)
|
||||
}
|
||||
|
||||
const loadProjectConfig = async (projectId: string) => {
|
||||
configLoading.value = true
|
||||
try {
|
||||
const res = await getProjectConfig(projectId)
|
||||
if (res.data?.data) {
|
||||
projectConfig.value = res.data.data
|
||||
}
|
||||
} catch {
|
||||
// 使用默认值
|
||||
} finally {
|
||||
configLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取项目列表
|
||||
|
|
@ -489,7 +532,7 @@ onMounted(fetchProjects)
|
|||
<Drawer
|
||||
v-model:open="editDrawerVisible"
|
||||
title="编辑项目"
|
||||
width="900px"
|
||||
width="1200px"
|
||||
:destroyOnClose="true"
|
||||
>
|
||||
<template v-if="editProject">
|
||||
|
|
@ -554,8 +597,59 @@ onMounted(fetchProjects)
|
|||
</div>
|
||||
<Table :columns="memberColumns" :dataSource="[]" :pagination="false" size="small" />
|
||||
</TabPane>
|
||||
<TabPane key="config" tab="功能配置">
|
||||
<Empty description="暂无配置数据" />
|
||||
<TabPane key="space" tab="空间管理">
|
||||
<div style="margin-bottom: 8px">
|
||||
<SpaceTree :projectId="editProject?.id" mode="edit" />
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane key="config" tab="项目配置">
|
||||
<Row :gutter="16">
|
||||
<Col :span="8">
|
||||
<Form.Item label="预约功能" name="enableReservation">
|
||||
<Switch v-model:checked="projectConfig.enableReservation" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Form.Item label="访客管理" name="enableVisitor">
|
||||
<Switch v-model:checked="projectConfig.enableVisitor" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Form.Item label="投诉建议" name="enableComplaint">
|
||||
<Switch v-model:checked="projectConfig.enableComplaint" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Form.Item label="缴费支付" name="enablePayment">
|
||||
<Switch v-model:checked="projectConfig.enablePayment" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Form.Item label="公告通知" name="enableAnnouncement">
|
||||
<Switch v-model:checked="projectConfig.enableAnnouncement" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Form.Item label="问卷调查" name="enableSurvey">
|
||||
<Switch v-model:checked="projectConfig.enableSurvey" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Form.Item label="投票表决" name="enableVote">
|
||||
<Switch v-model:checked="projectConfig.enableVote" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Form.Item label="设备维保" name="enableMaintenance">
|
||||
<Switch v-model:checked="projectConfig.enableMaintenance" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Form.Item label="资产管理" name="enableAsset">
|
||||
<Switch v-model:checked="projectConfig.enableAsset" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -2,14 +2,13 @@
|
|||
import { ref, onMounted } from 'vue'
|
||||
import { Table, Button, Space, Input, Select, DatePicker, Tag, message, ConfigProvider } from 'ant-design-vue'
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
||||
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||
import { SearchOutlined, ReloadOutlined, ExportOutlined } from '@ant-design/icons-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import { getAuditLogs, getAuditModules, getAuditActions, getAuditStats } from '@/api/audit'
|
||||
import type { AuditLog } from '@/api/audit'
|
||||
import {
|
||||
PageHeader,
|
||||
FilterBar,
|
||||
TableCard,
|
||||
TableToolbar,
|
||||
|
|
@ -212,6 +211,10 @@ const disabledDate = (current: Dayjs) => {
|
|||
return current && (current < thirtyDaysAgo || current > dayjs().endOf('day'))
|
||||
}
|
||||
|
||||
const handleExport = () => {
|
||||
message.info('导出功能开发中')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadModules()
|
||||
loadActions()
|
||||
|
|
@ -223,14 +226,16 @@ onMounted(() => {
|
|||
<template>
|
||||
<ConfigProvider :locale="zhCN">
|
||||
<div class="page-container">
|
||||
<PageHeader>
|
||||
<template #title>
|
||||
<span>操作审计日志</span>
|
||||
</template>
|
||||
<template #subtitle>
|
||||
<span>保留最近 {{ stats.retentionDays }} 天的操作记录,共 {{ stats.total }} 条</span>
|
||||
</template>
|
||||
</PageHeader>
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">操作审计日志</h2>
|
||||
<div class="page-header-actions">
|
||||
<span class="subtitle">保留最近 {{ stats.retentionDays }} 天的操作记录,共 {{ stats.total }} 条</span>
|
||||
<Button type="primary" @click="handleExport">
|
||||
<ExportOutlined /> 导出Excel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FilterBar>
|
||||
<Space wrap>
|
||||
|
|
@ -279,14 +284,7 @@ onMounted(() => {
|
|||
:data-source="logs"
|
||||
:loading="loading"
|
||||
:row-key="(record: AuditLog) => record.id"
|
||||
:pagination="{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: pagination.total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`
|
||||
}"
|
||||
@change="handleTableChange"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'module'">
|
||||
|
|
@ -321,3 +319,16 @@ onMounted(() => {
|
|||
</div>
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { ref, onMounted, reactive, computed, watch } from 'vue'
|
||||
import { Button, Drawer, Input, Select, Form, Space, message, Tabs, TabPane, Table, Tag, Checkbox, Avatar } from 'ant-design-vue'
|
||||
import { PlusOutlined, UserOutlined } from '@ant-design/icons-vue'
|
||||
import { getRoles, createRole, updateRole, deleteRole, getRolePermissions, assignPermissions, getRoleUsers } from '@/api/role'
|
||||
import { getRoles, getRole, createRole, updateRole, deleteRole, getRolePermissions, assignPermissions, getRoleUsers } from '@/api/role'
|
||||
import { getPermissions } from '@/api/permission'
|
||||
import type { Role, Permission, User } from '@/types'
|
||||
import {
|
||||
|
|
@ -253,19 +253,22 @@ const handleEdit = async (record: Role) => {
|
|||
}
|
||||
|
||||
const handleView = async (record: Role) => {
|
||||
drawerTitle.value = `查看角色 - ${record.name}`
|
||||
formState.value = {
|
||||
id: record.id,
|
||||
code: record.code,
|
||||
name: record.name,
|
||||
description: record.description || '',
|
||||
type: record.type || '',
|
||||
dataScope: record.dataScope || 'SELF',
|
||||
status: record.status
|
||||
}
|
||||
permissionsLoading.value = true
|
||||
roleUsersLoading.value = true
|
||||
viewDrawerVisible.value = true
|
||||
try {
|
||||
const res = await getRole(record.id)
|
||||
const roleData = res.data.data
|
||||
drawerTitle.value = `查看角色 - ${roleData.name}`
|
||||
formState.value = {
|
||||
id: roleData.id,
|
||||
code: roleData.code,
|
||||
name: roleData.name,
|
||||
description: roleData.description || '',
|
||||
type: roleData.type || '',
|
||||
dataScope: roleData.dataScope || 'SELF',
|
||||
status: roleData.status
|
||||
}
|
||||
permissionsLoading.value = true
|
||||
roleUsersLoading.value = true
|
||||
await Promise.all([
|
||||
fetchAllPermissions(),
|
||||
fetchRolePermissions(record.id),
|
||||
|
|
@ -275,7 +278,6 @@ const handleView = async (record: Role) => {
|
|||
permissionsLoading.value = false
|
||||
roleUsersLoading.value = false
|
||||
}
|
||||
viewDrawerVisible.value = true
|
||||
}
|
||||
|
||||
const fetchRoleUsers = async (roleId: string) => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import { ref, onMounted } from 'vue'
|
||||
import { Card, Form, FormItem, Input, Button, message, Divider } from 'ant-design-vue'
|
||||
import { SaveOutlined } from '@ant-design/icons-vue'
|
||||
import { PageHeader } from '@/components'
|
||||
import { getConfig, updateConfig } from '@/api/system'
|
||||
|
||||
const loading = ref(false)
|
||||
|
|
@ -59,13 +58,12 @@ const handleSubmit = async () => {
|
|||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<PageHeader>
|
||||
<template #title>
|
||||
<span>系统设置</span>
|
||||
</template>
|
||||
</PageHeader>
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">系统设置</h2>
|
||||
</div>
|
||||
|
||||
<Card :loading="loading">
|
||||
<Card :loading="loading" class="settings-card">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
|
|
@ -114,3 +112,11 @@ const handleSubmit = async () => {
|
|||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.settings-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { ref, onMounted, reactive, computed } from 'vue'
|
||||
import { Button, Drawer, Form, Space, message, Tag, Checkbox, Spin, Descriptions, DescriptionsItem } from 'ant-design-vue'
|
||||
import { PlusOutlined, SafetyOutlined } from '@ant-design/icons-vue'
|
||||
import { getUsers, createUser, updateUser, deleteUser, assignRoles } from '@/api/user'
|
||||
import { getUsers, getUser, createUser, updateUser, deleteUser, assignRoles } from '@/api/user'
|
||||
import { getRoles } from '@/api/role'
|
||||
import type { User, Role } from '@/types'
|
||||
import {
|
||||
|
|
@ -180,10 +180,15 @@ const viewDrawerVisible = ref(false)
|
|||
const viewDrawerTitle = ref('')
|
||||
const viewUser = ref<User | null>(null)
|
||||
|
||||
const handleView = (record: User) => {
|
||||
viewUser.value = record
|
||||
const handleView = async (record: User) => {
|
||||
viewDrawerTitle.value = `查看用户 - ${record.realName || record.username}`
|
||||
viewDrawerVisible.value = true
|
||||
try {
|
||||
const res = await getUser(record.id)
|
||||
viewUser.value = res.data.data
|
||||
} catch {
|
||||
viewUser.value = record
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewClose = () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue