feat: add equipment health and inspection template frontend pages

This commit is contained in:
chiguyong 2026-03-24 01:02:34 +08:00
parent 5c7728e3db
commit 7b3194219b
17 changed files with 1706 additions and 50 deletions

25
node_modules/.package-lock.json generated vendored
View File

@ -822,6 +822,16 @@
"node": ">= 0.4" "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": { "node_modules/entities": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
@ -1799,6 +1809,12 @@
"node": ">=14.0.0" "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": { "node_modules/type-detect": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmmirror.com/type-detect/-/type-detect-4.1.0.tgz", "resolved": "https://registry.npmmirror.com/type-detect/-/type-detect-4.1.0.tgz",
@ -2152,6 +2168,15 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "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"
}
} }
} }
} }

26
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"ant-design-vue": "^4.2.6", "ant-design-vue": "^4.2.6",
"axios": "^1.7.0", "axios": "^1.7.0",
"echarts": "^6.0.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.0", "vue": "^3.4.0",
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
@ -1552,6 +1553,16 @@
"node": ">= 0.4" "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": { "node_modules/entities": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
@ -2529,6 +2540,12 @@
"node": ">=14.0.0" "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": { "node_modules/type-detect": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmmirror.com/type-detect/-/type-detect-4.1.0.tgz", "resolved": "https://registry.npmmirror.com/type-detect/-/type-detect-4.1.0.tgz",
@ -2882,6 +2899,15 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "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"
}
} }
} }
} }

View File

@ -13,6 +13,7 @@
"dependencies": { "dependencies": {
"ant-design-vue": "^4.2.6", "ant-design-vue": "^4.2.6",
"axios": "^1.7.0", "axios": "^1.7.0",
"echarts": "^6.0.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.0", "vue": "^3.4.0",
"vue-router": "^4.3.0" "vue-router": "^4.3.0"

106
src/api/equipment-health.ts Normal file
View File

@ -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}`)
}

View File

@ -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}`)
}

View File

@ -31,7 +31,9 @@ const handleBack = () => {
</svg> </svg>
</button> </button>
<h2 v-if="title" class="page-title">{{ title }}</h2> <h2 v-if="title" class="page-title">{{ title }}</h2>
<template v-else>
<slot name="title"></slot> <slot name="title"></slot>
</template>
</div> </div>
<div v-if="actions && actions.length > 0" class="page-header-actions"> <div v-if="actions && actions.length > 0" class="page-header-actions">
<button <button

View File

@ -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>

View File

@ -63,3 +63,6 @@ export { default as PasswordItem } from './PasswordItem/index.vue'
// 业务组件 - 详情 // 业务组件 - 详情
export { default as DescriptionList } from './DescriptionList/index.vue' export { default as DescriptionList } from './DescriptionList/index.vue'
export { default as ProfileCard } from './ProfileCard/index.vue' export { default as ProfileCard } from './ProfileCard/index.vue'
// 业务组件 - 空间
export { default as SpaceTree } from './SpaceTree/index.vue'

View File

@ -81,6 +81,18 @@ const router = createRouter({
component: () => import('@/views/equipment/EquipmentDetail.vue'), component: () => import('@/views/equipment/EquipmentDetail.vue'),
meta: { title: '设备详情' } 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', path: 'maintenance/plans',
name: 'MaintenancePlans', name: 'MaintenancePlans',

View File

@ -39,7 +39,9 @@ const menuItems: MenuProps['items'] = [
type: 'group', type: 'group',
children: [ children: [
{ key: '/project/list', icon: () => h(BuildOutlined), label: '项目管理' }, { 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: '点检模板' }
] ]
}, },
{ {

View File

@ -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>

View File

@ -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>

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue' import { ref, reactive, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router' 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 type { ColumnsType } from 'ant-design-vue/es/table'
import { import {
PlusOutlined, PlusOutlined,
@ -16,9 +16,11 @@ import {
deleteProject, deleteProject,
enableProject, enableProject,
disableProject, disableProject,
getProjectStatistics getProjectStatistics,
getProjectConfig,
updateProjectConfig
} from '@/api/project' } from '@/api/project'
import { TableActions, Pagination, StatusTag } from '@/components' import { TableActions, Pagination, StatusTag, SpaceTree } from '@/components'
import type { Project } from '@/types' import type { Project } from '@/types'
import type { ProjectQuery, ProjectStatus, ProjectFormData, PageResponse, ProjectType } from '@/types/project' import type { ProjectQuery, ProjectStatus, ProjectFormData, PageResponse, ProjectType } from '@/types/project'
import { ProjectStatusMap, ProjectTypeMap } from '@/types/project' import { ProjectStatusMap, ProjectTypeMap } from '@/types/project'
@ -126,6 +128,22 @@ const editFormState = ref({
}) })
const editSubmitting = ref(false) 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({ const createFormState = ref({
name: '', name: '',
@ -163,6 +181,31 @@ const handleEdit = async (record: Project) => {
editProject.value = record editProject.value = record
editActiveTab.value = 'info' editActiveTab.value = 'info'
editDrawerVisible.value = true 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 <Drawer
v-model:open="editDrawerVisible" v-model:open="editDrawerVisible"
title="编辑项目" title="编辑项目"
width="900px" width="1200px"
:destroyOnClose="true" :destroyOnClose="true"
> >
<template v-if="editProject"> <template v-if="editProject">
@ -554,8 +597,59 @@ onMounted(fetchProjects)
</div> </div>
<Table :columns="memberColumns" :dataSource="[]" :pagination="false" size="small" /> <Table :columns="memberColumns" :dataSource="[]" :pagination="false" size="small" />
</TabPane> </TabPane>
<TabPane key="config" tab="功能配置"> <TabPane key="space" tab="空间管理">
<Empty description="暂无配置数据" /> <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> </TabPane>
</Tabs> </Tabs>
</template> </template>

View File

@ -2,14 +2,13 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { Table, Button, Space, Input, Select, DatePicker, Tag, message, ConfigProvider } from 'ant-design-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 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 from 'dayjs'
import 'dayjs/locale/zh-cn' import 'dayjs/locale/zh-cn'
import type { Dayjs } from 'dayjs' import type { Dayjs } from 'dayjs'
import { getAuditLogs, getAuditModules, getAuditActions, getAuditStats } from '@/api/audit' import { getAuditLogs, getAuditModules, getAuditActions, getAuditStats } from '@/api/audit'
import type { AuditLog } from '@/api/audit' import type { AuditLog } from '@/api/audit'
import { import {
PageHeader,
FilterBar, FilterBar,
TableCard, TableCard,
TableToolbar, TableToolbar,
@ -212,6 +211,10 @@ const disabledDate = (current: Dayjs) => {
return current && (current < thirtyDaysAgo || current > dayjs().endOf('day')) return current && (current < thirtyDaysAgo || current > dayjs().endOf('day'))
} }
const handleExport = () => {
message.info('导出功能开发中')
}
onMounted(() => { onMounted(() => {
loadModules() loadModules()
loadActions() loadActions()
@ -223,14 +226,16 @@ onMounted(() => {
<template> <template>
<ConfigProvider :locale="zhCN"> <ConfigProvider :locale="zhCN">
<div class="page-container"> <div class="page-container">
<PageHeader> <!-- 页面标题 -->
<template #title> <div class="page-header">
<span>操作审计日志</span> <h2 class="page-title">操作审计日志</h2>
</template> <div class="page-header-actions">
<template #subtitle> <span class="subtitle">保留最近 {{ stats.retentionDays }} 天的操作记录 {{ stats.total }} </span>
<span>保留最近 {{ stats.retentionDays }} 天的操作记录 {{ stats.total }} </span> <Button type="primary" @click="handleExport">
</template> <ExportOutlined /> 导出Excel
</PageHeader> </Button>
</div>
</div>
<FilterBar> <FilterBar>
<Space wrap> <Space wrap>
@ -279,14 +284,7 @@ onMounted(() => {
:data-source="logs" :data-source="logs"
:loading="loading" :loading="loading"
:row-key="(record: AuditLog) => record.id" :row-key="(record: AuditLog) => record.id"
:pagination="{ :pagination="false"
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total,
showSizeChanger: true,
showTotal: (total: number) => `${total}`
}"
@change="handleTableChange"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'module'"> <template v-if="column.key === 'module'">
@ -321,3 +319,16 @@ onMounted(() => {
</div> </div>
</ConfigProvider> </ConfigProvider>
</template> </template>
<style scoped>
.page-header-actions {
display: flex;
align-items: center;
gap: 16px;
}
.subtitle {
font-size: 14px;
color: #8c8c8c;
}
</style>

View File

@ -2,7 +2,7 @@
import { ref, onMounted, reactive, computed, watch } from 'vue' 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 { 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 { 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 { getPermissions } from '@/api/permission'
import type { Role, Permission, User } from '@/types' import type { Role, Permission, User } from '@/types'
import { import {
@ -253,19 +253,22 @@ const handleEdit = async (record: Role) => {
} }
const handleView = async (record: Role) => { const handleView = async (record: Role) => {
drawerTitle.value = `查看角色 - ${record.name}` viewDrawerVisible.value = true
try {
const res = await getRole(record.id)
const roleData = res.data.data
drawerTitle.value = `查看角色 - ${roleData.name}`
formState.value = { formState.value = {
id: record.id, id: roleData.id,
code: record.code, code: roleData.code,
name: record.name, name: roleData.name,
description: record.description || '', description: roleData.description || '',
type: record.type || '', type: roleData.type || '',
dataScope: record.dataScope || 'SELF', dataScope: roleData.dataScope || 'SELF',
status: record.status status: roleData.status
} }
permissionsLoading.value = true permissionsLoading.value = true
roleUsersLoading.value = true roleUsersLoading.value = true
try {
await Promise.all([ await Promise.all([
fetchAllPermissions(), fetchAllPermissions(),
fetchRolePermissions(record.id), fetchRolePermissions(record.id),
@ -275,7 +278,6 @@ const handleView = async (record: Role) => {
permissionsLoading.value = false permissionsLoading.value = false
roleUsersLoading.value = false roleUsersLoading.value = false
} }
viewDrawerVisible.value = true
} }
const fetchRoleUsers = async (roleId: string) => { const fetchRoleUsers = async (roleId: string) => {

View File

@ -2,7 +2,6 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { Card, Form, FormItem, Input, Button, message, Divider } from 'ant-design-vue' import { Card, Form, FormItem, Input, Button, message, Divider } from 'ant-design-vue'
import { SaveOutlined } from '@ant-design/icons-vue' import { SaveOutlined } from '@ant-design/icons-vue'
import { PageHeader } from '@/components'
import { getConfig, updateConfig } from '@/api/system' import { getConfig, updateConfig } from '@/api/system'
const loading = ref(false) const loading = ref(false)
@ -59,13 +58,12 @@ const handleSubmit = async () => {
<template> <template>
<div class="page-container"> <div class="page-container">
<PageHeader> <!-- 页面标题 -->
<template #title> <div class="page-header">
<span>系统设置</span> <h2 class="page-title">系统设置</h2>
</template> </div>
</PageHeader>
<Card :loading="loading"> <Card :loading="loading" class="settings-card">
<Form <Form
ref="formRef" ref="formRef"
:model="formState" :model="formState"
@ -114,3 +112,11 @@ const handleSubmit = async () => {
</Card> </Card>
</div> </div>
</template> </template>
<style scoped>
.settings-card {
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 8px;
}
</style>

View File

@ -2,7 +2,7 @@
import { ref, onMounted, reactive, computed } from 'vue' import { ref, onMounted, reactive, computed } from 'vue'
import { Button, Drawer, Form, Space, message, Tag, Checkbox, Spin, Descriptions, DescriptionsItem } from 'ant-design-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 { 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 { getRoles } from '@/api/role'
import type { User, Role } from '@/types' import type { User, Role } from '@/types'
import { import {
@ -180,10 +180,15 @@ const viewDrawerVisible = ref(false)
const viewDrawerTitle = ref('') const viewDrawerTitle = ref('')
const viewUser = ref<User | null>(null) const viewUser = ref<User | null>(null)
const handleView = (record: User) => { const handleView = async (record: User) => {
viewUser.value = record
viewDrawerTitle.value = `查看用户 - ${record.realName || record.username}` viewDrawerTitle.value = `查看用户 - ${record.realName || record.username}`
viewDrawerVisible.value = true viewDrawerVisible.value = true
try {
const res = await getUser(record.id)
viewUser.value = res.data.data
} catch {
viewUser.value = record
}
} }
const handleViewClose = () => { const handleViewClose = () => {