feat: add maintenance management frontend pages
This commit is contained in:
parent
7956379f71
commit
913d6400e4
|
|
@ -0,0 +1,156 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// ==================== 维保计划相关类型 ====================
|
||||
|
||||
export interface MaintenancePlan {
|
||||
id: string
|
||||
name: string
|
||||
projectId: string
|
||||
projectName?: string
|
||||
triggerType: 'MANUAL' | 'SCHEDULED' | 'AUTOMATIC'
|
||||
equipmentId?: string
|
||||
equipmentName?: string
|
||||
spaceNodeId?: string
|
||||
spaceNodeName?: string
|
||||
description?: string
|
||||
enabled: boolean
|
||||
cronExpression?: string
|
||||
nextTriggerTime?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export interface MaintenancePlanForm {
|
||||
id?: string
|
||||
name: string
|
||||
projectId: string
|
||||
triggerType: 'MANUAL' | 'SCHEDULED' | 'AUTOMATIC'
|
||||
equipmentId?: string
|
||||
spaceNodeId?: string
|
||||
description?: string
|
||||
enabled?: boolean
|
||||
cronExpression?: string
|
||||
}
|
||||
|
||||
// ==================== 维保任务相关类型 ====================
|
||||
|
||||
export type TaskStatus = 'PENDING' | 'ACCEPTED' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED'
|
||||
|
||||
export interface MaintenanceTask {
|
||||
id: string
|
||||
planId?: string
|
||||
planName?: string
|
||||
projectId: string
|
||||
projectName?: string
|
||||
equipmentId?: string
|
||||
equipmentName?: string
|
||||
spaceNodeId?: string
|
||||
spaceNodeName?: string
|
||||
title: string
|
||||
description?: string
|
||||
status: TaskStatus
|
||||
assigneeId?: string
|
||||
assigneeName?: string
|
||||
scheduledDate?: string
|
||||
startTime?: string
|
||||
completedTime?: string
|
||||
cancellationReason?: string
|
||||
completionNotes?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export interface TaskQueryParams {
|
||||
projectId?: string
|
||||
status?: TaskStatus
|
||||
assigneeId?: string
|
||||
page?: number
|
||||
size?: number
|
||||
}
|
||||
|
||||
// ==================== 维保计划 API ====================
|
||||
|
||||
// 获取维保计划列表
|
||||
export function getMaintenancePlans(projectId: string, triggerType?: string) {
|
||||
return request.get<MaintenancePlan[]>({
|
||||
url: '/api/v1/ops/maintenance-plans',
|
||||
params: { projectId, triggerType }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取维保计划详情
|
||||
export function getMaintenancePlan(id: string) {
|
||||
return request.get<MaintenancePlan>({
|
||||
url: `/api/v1/ops/maintenance-plans/${id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 创建维保计划
|
||||
export function createMaintenancePlan(data: MaintenancePlanForm) {
|
||||
return request.post({
|
||||
url: '/api/v1/ops/maintenance-plans',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新维保计划
|
||||
export function updateMaintenancePlan(id: string, data: MaintenancePlanForm) {
|
||||
return request.put({
|
||||
url: `/api/v1/ops/maintenance-plans/${id}`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除/停用维保计划
|
||||
export function deleteMaintenancePlan(id: string) {
|
||||
return request.delete({
|
||||
url: `/api/v1/ops/maintenance-plans/${id}`
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 维保任务 API ====================
|
||||
|
||||
// 获取维保任务列表
|
||||
export function getMaintenanceTasks(params: TaskQueryParams) {
|
||||
return request.get({
|
||||
url: '/api/v1/ops/maintenance-tasks',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取维保任务详情
|
||||
export function getMaintenanceTask(id: string) {
|
||||
return request.get<MaintenanceTask>({
|
||||
url: `/api/v1/ops/maintenance-tasks/${id}`
|
||||
})
|
||||
}
|
||||
|
||||
// 接受任务
|
||||
export function acceptMaintenanceTask(id: string, userId: string) {
|
||||
return request.post({
|
||||
url: `/api/v1/ops/maintenance-tasks/${id}/accept`,
|
||||
params: { userId }
|
||||
})
|
||||
}
|
||||
|
||||
// 开始执行任务
|
||||
export function startMaintenanceTask(id: string) {
|
||||
return request.post({
|
||||
url: `/api/v1/ops/maintenance-tasks/${id}/start`
|
||||
})
|
||||
}
|
||||
|
||||
// 完成维保
|
||||
export function completeMaintenanceTask(id: string, data: { completionNotes?: string }) {
|
||||
return request.post({
|
||||
url: `/api/v1/ops/maintenance-tasks/${id}/complete`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 取消任务
|
||||
export function cancelMaintenanceTask(id: string) {
|
||||
return request.post({
|
||||
url: `/api/v1/ops/maintenance-tasks/${id}/cancel`
|
||||
})
|
||||
}
|
||||
|
|
@ -2,12 +2,18 @@
|
|||
import { useRouter } from 'vue-router'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
title?: string
|
||||
showBack?: boolean
|
||||
actions?: { icon: string; text: string; onClick?: () => void }[]
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: ''
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
(e: 'back'): void
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
|
|
@ -24,7 +30,8 @@ const handleBack = () => {
|
|||
<path d="M12.5 15L7.5 10L12.5 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<h2 class="page-title">{{ title }}</h2>
|
||||
<h2 v-if="title" class="page-title">{{ title }}</h2>
|
||||
<slot name="title"></slot>
|
||||
</div>
|
||||
<div v-if="actions && actions.length > 0" class="page-header-actions">
|
||||
<button
|
||||
|
|
@ -36,6 +43,7 @@ const handleBack = () => {
|
|||
{{ action.text }}
|
||||
</button>
|
||||
</div>
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -80,6 +80,18 @@ const router = createRouter({
|
|||
name: 'EquipmentDetail',
|
||||
component: () => import('@/views/equipment/EquipmentDetail.vue'),
|
||||
meta: { title: '设备详情' }
|
||||
},
|
||||
{
|
||||
path: 'maintenance/plans',
|
||||
name: 'MaintenancePlans',
|
||||
component: () => import('@/views/maintenance/PlanList.vue'),
|
||||
meta: { title: '维保计划' }
|
||||
},
|
||||
{
|
||||
path: 'maintenance/tasks',
|
||||
name: 'MaintenanceTasks',
|
||||
component: () => import('@/views/maintenance/TaskList.vue'),
|
||||
meta: { title: '维保任务' }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,18 @@ const menuItems: MenuProps['items'] = [
|
|||
key: 'operation',
|
||||
label: '运营管理',
|
||||
type: 'group',
|
||||
children: []
|
||||
children: [
|
||||
{
|
||||
key: '/maintenance/plans',
|
||||
icon: () => h(ToolOutlined),
|
||||
label: '维保计划'
|
||||
},
|
||||
{
|
||||
key: '/maintenance/tasks',
|
||||
icon: () => h(ToolOutlined),
|
||||
label: '维保任务'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'system',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,397 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { Button, Select, Space, message, Tag, Modal, Form, Input, InputNumber, Popconfirm } from 'ant-design-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import {
|
||||
getMaintenancePlans,
|
||||
createMaintenancePlan,
|
||||
updateMaintenancePlan,
|
||||
deleteMaintenancePlan,
|
||||
type MaintenancePlan,
|
||||
type MaintenancePlanForm
|
||||
} from '@/api/maintenance'
|
||||
import { getProjectSelectorList } from '@/api/project'
|
||||
import { TableActions, Pagination } from '@/components'
|
||||
|
||||
// 触发类型映射
|
||||
const triggerTypeMap: Record<string, { text: string; color: string }> = {
|
||||
MANUAL: { text: '手动', color: 'default' },
|
||||
SCHEDULED: { text: '定时', color: 'blue' },
|
||||
AUTOMATIC: { text: '自动', color: 'green' }
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns: ColumnsType = [
|
||||
{ title: '计划名称', dataIndex: 'name', key: 'name', width: 180 },
|
||||
{ title: '所属项目', dataIndex: 'projectName', key: 'projectName', width: 150 },
|
||||
{ title: '触发类型', dataIndex: 'triggerType', key: 'triggerType', width: 100 },
|
||||
{ title: '关联设备', dataIndex: 'equipmentName', key: 'equipmentName', width: 150 },
|
||||
{ title: 'Cron表达式', dataIndex: 'cronExpression', key: 'cronExpression', width: 120 },
|
||||
{ title: '下次触发时间', dataIndex: 'nextTriggerTime', key: 'nextTriggerTime', width: 160 },
|
||||
{ title: '状态', dataIndex: 'enabled', key: 'enabled', width: 80 },
|
||||
{ title: '操作', key: 'action', width: 120, fixed: 'right' as const }
|
||||
]
|
||||
|
||||
// 项目选择选项
|
||||
const projectOptions = ref<{ value: string; label: string }[]>([])
|
||||
|
||||
// 触发类型选项
|
||||
const triggerTypeOptions = [
|
||||
{ value: 'MANUAL', label: '手动' },
|
||||
{ value: 'SCHEDULED', label: '定时' },
|
||||
{ value: 'AUTOMATIC', label: '自动' }
|
||||
]
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
projectId: '',
|
||||
triggerType: undefined as string | undefined
|
||||
})
|
||||
|
||||
// 数据状态
|
||||
const loading = ref(false)
|
||||
const tableData = ref<MaintenancePlan[]>([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 模态框状态
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('新建维保计划')
|
||||
const formLoading = ref(false)
|
||||
const editingPlan = ref<MaintenancePlan | null>(null)
|
||||
|
||||
// 表单数据
|
||||
const formState = reactive<MaintenancePlanForm>({
|
||||
name: '',
|
||||
projectId: '',
|
||||
triggerType: 'MANUAL',
|
||||
equipmentId: undefined,
|
||||
spaceNodeId: undefined,
|
||||
description: '',
|
||||
enabled: true,
|
||||
cronExpression: ''
|
||||
})
|
||||
|
||||
// 获取项目列表
|
||||
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 fetchPlanList = async () => {
|
||||
if (!queryParams.projectId) {
|
||||
message.warning('请先选择项目')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getMaintenancePlans(queryParams.projectId, queryParams.triggerType)
|
||||
const data = res.data.data || []
|
||||
tableData.value = data
|
||||
pagination.total = data.length
|
||||
} catch {
|
||||
message.error('获取维保计划列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchPlanList()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
queryParams.projectId = ''
|
||||
queryParams.triggerType = undefined
|
||||
pagination.current = 1
|
||||
tableData.value = []
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
pagination.current = page
|
||||
pagination.pageSize = pageSize
|
||||
fetchPlanList()
|
||||
}
|
||||
|
||||
// 打开新建模态框
|
||||
const handleAdd = () => {
|
||||
editingPlan.value = null
|
||||
modalTitle.value = '新建维保计划'
|
||||
formState.name = ''
|
||||
formState.projectId = queryParams.projectId
|
||||
formState.triggerType = 'MANUAL'
|
||||
formState.equipmentId = undefined
|
||||
formState.spaceNodeId = undefined
|
||||
formState.description = ''
|
||||
formState.enabled = true
|
||||
formState.cronExpression = ''
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 打开编辑模态框
|
||||
const handleEdit = (record: MaintenancePlan) => {
|
||||
editingPlan.value = record
|
||||
modalTitle.value = '编辑维保计划'
|
||||
formState.id = record.id
|
||||
formState.name = record.name
|
||||
formState.projectId = record.projectId
|
||||
formState.triggerType = record.triggerType
|
||||
formState.equipmentId = record.equipmentId
|
||||
formState.spaceNodeId = record.spaceNodeId
|
||||
formState.description = record.description || ''
|
||||
formState.enabled = record.enabled
|
||||
formState.cronExpression = record.cronExpression || ''
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (editingPlan.value) {
|
||||
await updateMaintenancePlan(editingPlan.value.id, formState)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await createMaintenancePlan(formState)
|
||||
message.success('创建成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
fetchPlanList()
|
||||
} catch {
|
||||
message.error(editingPlan.value ? '更新失败' : '创建失败')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除计划
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await deleteMaintenancePlan(id)
|
||||
message.success('停用成功')
|
||||
fetchPlanList()
|
||||
} catch {
|
||||
message.error('停用失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date: string | Date | undefined) => {
|
||||
if (!date) return '-'
|
||||
const d = new Date(date)
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
const hour = String(d.getHours()).padStart(2, '0')
|
||||
const minute = String(d.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
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"
|
||||
allow-clear
|
||||
:options="projectOptions"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="queryParams.triggerType"
|
||||
placeholder="触发类型"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
:options="triggerTypeOptions"
|
||||
/>
|
||||
<Button type="primary" @click="handleSearch">
|
||||
<SearchOutlined /> 查询
|
||||
</Button>
|
||||
<Button @click="handleReset">
|
||||
<ReloadOutlined /> 重置
|
||||
</Button>
|
||||
<Button type="primary" @click="handleAdd">
|
||||
<PlusOutlined /> 新建
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 表格区 -->
|
||||
<div class="table-card">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:row-key="(record: MaintenancePlan) => 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 === 'triggerType'">
|
||||
<Tag :color="triggerTypeMap[record.triggerType]?.color">
|
||||
{{ triggerTypeMap[record.triggerType]?.text }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'nextTriggerTime'">
|
||||
{{ formatDate(record.nextTriggerTime) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'enabled'">
|
||||
<Tag :color="record.enabled ? 'green' : 'red'">
|
||||
{{ record.enabled ? '启用' : '停用' }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button type="link" size="small" @click="handleEdit(record)">
|
||||
<EditOutlined /> 编辑
|
||||
</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="!queryParams.projectId && tableData.length === 0" description="请先选择项目" />
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑模态框 -->
|
||||
<Modal
|
||||
v-model:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:footer="null"
|
||||
width="600px"
|
||||
@cancel="modalVisible = false"
|
||||
>
|
||||
<Form
|
||||
:model="formState"
|
||||
layout="vertical"
|
||||
@finish="handleSubmit"
|
||||
>
|
||||
<Form.Item label="计划名称" name="name" :rules="[{ required: true, message: '请输入计划名称' }]">
|
||||
<Input v-model:value="formState.name" placeholder="请输入计划名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="所属项目" name="projectId" :rules="[{ required: true, message: '请选择项目' }]">
|
||||
<Select
|
||||
v-model:value="formState.projectId"
|
||||
placeholder="请选择项目"
|
||||
:options="projectOptions"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="触发类型" name="triggerType" :rules="[{ required: true, message: '请选择触发类型' }]">
|
||||
<Select
|
||||
v-model:value="formState.triggerType"
|
||||
placeholder="请选择触发类型"
|
||||
:options="triggerTypeOptions"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Cron表达式" name="cronExpression">
|
||||
<Input v-model:value="formState.cronExpression" placeholder="如: 0 0 2 * * ?" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="描述" name="description">
|
||||
<Input.TextArea v-model:value="formState.description" placeholder="请输入描述" :rows="3" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="启用状态" name="enabled">
|
||||
<Select
|
||||
v-model:value="formState.enabled"
|
||||
:options="[
|
||||
{ value: true, label: '启用' },
|
||||
{ value: false, label: '停用' }
|
||||
]"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style="margin-bottom: 0; text-align: right">
|
||||
<Space>
|
||||
<Button @click="modalVisible = false">取消</Button>
|
||||
<Button type="primary" html-type="submit" :loading="formLoading">
|
||||
确定
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</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: 24px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,401 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Button, Select, Space, message, Badge, Modal, Input, Form, Popconfirm } from 'ant-design-vue'
|
||||
import type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
CheckCircleOutlined,
|
||||
PlayCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
UserOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import {
|
||||
getMaintenanceTasks,
|
||||
acceptMaintenanceTask,
|
||||
startMaintenanceTask,
|
||||
completeMaintenanceTask,
|
||||
cancelMaintenanceTask,
|
||||
type MaintenanceTask,
|
||||
type TaskStatus
|
||||
} from '@/api/maintenance'
|
||||
import { getProjectSelectorList } from '@/api/project'
|
||||
import { getUserList } from '@/api/user'
|
||||
|
||||
// 任务状态映射
|
||||
const statusMap: Record<TaskStatus, { text: string; color: string; status: 'default' | 'processing' | 'success' | 'error' | 'warning' | 'default' }> = {
|
||||
PENDING: { text: '待接受', color: 'default', status: 'default' },
|
||||
ACCEPTED: { text: '已接受', color: 'blue', status: 'processing' },
|
||||
IN_PROGRESS: { text: '进行中', color: 'processing', status: 'processing' },
|
||||
COMPLETED: { text: '已完成', color: 'success', status: 'success' },
|
||||
CANCELLED: { text: '已取消', color: 'error', status: 'error' }
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns: ColumnsType = [
|
||||
{ title: '任务标题', dataIndex: 'title', key: 'title', width: 180 },
|
||||
{ title: '所属项目', dataIndex: 'projectName', key: 'projectName', width: 150 },
|
||||
{ title: '关联设备', dataIndex: 'equipmentName', key: 'equipmentName', width: 150 },
|
||||
{ title: '负责人', dataIndex: 'assigneeName', key: 'assigneeName', width: 100 },
|
||||
{ title: '计划日期', dataIndex: 'scheduledDate', key: 'scheduledDate', width: 120 },
|
||||
{ title: '开始时间', dataIndex: 'startTime', key: 'startTime', width: 160 },
|
||||
{ title: '完成时间', dataIndex: 'completedTime', key: 'completedTime', width: 160 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
|
||||
{ title: '操作', key: 'action', width: 200, fixed: 'right' as const }
|
||||
]
|
||||
|
||||
// 项目选择选项
|
||||
const projectOptions = ref<{ value: string; label: string }[]>([])
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ value: 'PENDING', label: '待接受' },
|
||||
{ value: 'ACCEPTED', label: '已接受' },
|
||||
{ value: 'IN_PROGRESS', label: '进行中' },
|
||||
{ value: 'COMPLETED', label: '已完成' },
|
||||
{ value: 'CANCELLED', label: '取消' }
|
||||
]
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
projectId: '',
|
||||
status: undefined as TaskStatus | undefined,
|
||||
assigneeId: undefined as string | undefined
|
||||
})
|
||||
|
||||
// 数据状态
|
||||
const loading = ref(false)
|
||||
const tableData = ref<MaintenanceTask[]>([])
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 取消模态框
|
||||
const cancelModalVisible = ref(false)
|
||||
const cancelReason = ref('')
|
||||
const cancellingTaskId = ref<string | null>(null)
|
||||
|
||||
// 完成模态框
|
||||
const completeModalVisible = ref(false)
|
||||
const completionNotes = ref('')
|
||||
const completingTaskId = ref<string | null>(null)
|
||||
|
||||
// 获取项目列表
|
||||
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 fetchTaskList = async () => {
|
||||
if (!queryParams.projectId) {
|
||||
message.warning('请先选择项目')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getMaintenanceTasks({
|
||||
projectId: queryParams.projectId,
|
||||
status: queryParams.status,
|
||||
assigneeId: queryParams.assigneeId
|
||||
})
|
||||
const data = res.data.data || []
|
||||
tableData.value = data
|
||||
pagination.total = data.length
|
||||
} catch {
|
||||
message.error('获取维保任务列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchTaskList()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
queryParams.projectId = ''
|
||||
queryParams.status = undefined
|
||||
queryParams.assigneeId = undefined
|
||||
pagination.current = 1
|
||||
tableData.value = []
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
pagination.current = page
|
||||
pagination.pageSize = pageSize
|
||||
fetchTaskList()
|
||||
}
|
||||
|
||||
// 接受任务
|
||||
const handleAccept = async (id: string) => {
|
||||
try {
|
||||
await acceptMaintenanceTask(id, 'current-user-id')
|
||||
message.success('任务已接受')
|
||||
fetchTaskList()
|
||||
} catch {
|
||||
message.error('接受任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 开始任务
|
||||
const handleStart = async (id: string) => {
|
||||
try {
|
||||
await startMaintenanceTask(id)
|
||||
message.success('任务已开始')
|
||||
fetchTaskList()
|
||||
} catch {
|
||||
message.error('开始任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 打开完成模态框
|
||||
const handleOpenComplete = (id: string) => {
|
||||
completingTaskId.value = id
|
||||
completionNotes.value = ''
|
||||
completeModalVisible.value = true
|
||||
}
|
||||
|
||||
// 完成任务
|
||||
const handleComplete = async () => {
|
||||
if (!completingTaskId.value) return
|
||||
try {
|
||||
await completeMaintenanceTask(completingTaskId.value, { completionNotes: completionNotes.value })
|
||||
message.success('任务已完成')
|
||||
completeModalVisible.value = false
|
||||
completingTaskId.value = null
|
||||
completionNotes.value = ''
|
||||
fetchTaskList()
|
||||
} catch {
|
||||
message.error('完成任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 打开取消模态框
|
||||
const handleOpenCancel = (id: string) => {
|
||||
cancellingTaskId.value = id
|
||||
cancelReason.value = ''
|
||||
cancelModalVisible.value = true
|
||||
}
|
||||
|
||||
// 取消任务
|
||||
const handleCancel = async () => {
|
||||
if (!cancellingTaskId.value) return
|
||||
try {
|
||||
await cancelMaintenanceTask(cancellingTaskId.value)
|
||||
message.success('任务已取消')
|
||||
cancelModalVisible.value = false
|
||||
cancellingTaskId.value = null
|
||||
cancelReason.value = ''
|
||||
fetchTaskList()
|
||||
} catch {
|
||||
message.error('取消任务失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (date: string | Date | undefined) => {
|
||||
if (!date) return '-'
|
||||
const d = new Date(date)
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
const hour = String(d.getHours()).padStart(2, '0')
|
||||
const minute = String(d.getMinutes()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
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"
|
||||
allow-clear
|
||||
:options="projectOptions"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="queryParams.status"
|
||||
placeholder="任务状态"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
:options="statusOptions"
|
||||
/>
|
||||
<Button type="primary" @click="handleSearch">
|
||||
<SearchOutlined /> 查询
|
||||
</Button>
|
||||
<Button @click="handleReset">
|
||||
<ReloadOutlined /> 重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<!-- 表格区 -->
|
||||
<div class="table-card">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:row-key="(record: MaintenanceTask) => 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 === 'assigneeName'">
|
||||
<Space>
|
||||
<UserOutlined />
|
||||
{{ record.assigneeName || '-' }}
|
||||
</Space>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'scheduledDate'">
|
||||
{{ record.scheduledDate || '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'startTime'">
|
||||
{{ formatDateTime(record.startTime) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'completedTime'">
|
||||
{{ formatDateTime(record.completedTime) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<Badge
|
||||
:status="statusMap[record.status]?.status"
|
||||
:text="statusMap[record.status]?.text"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Space>
|
||||
<!-- 待接受状态显示接受按钮 -->
|
||||
<Button
|
||||
v-if="record.status === 'PENDING'"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleAccept(record.id)"
|
||||
>
|
||||
<CheckCircleOutlined /> 接受
|
||||
</Button>
|
||||
<!-- 已接受状态显示开始按钮 -->
|
||||
<Button
|
||||
v-if="record.status === 'ACCEPTED'"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleStart(record.id)"
|
||||
>
|
||||
<PlayCircleOutlined /> 开始
|
||||
</Button>
|
||||
<!-- 进行中状态显示完成按钮 -->
|
||||
<Button
|
||||
v-if="record.status === 'IN_PROGRESS'"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleOpenComplete(record.id)"
|
||||
>
|
||||
<CheckCircleOutlined /> 完成
|
||||
</Button>
|
||||
<!-- 待接受、已接受、进行中状态显示取消按钮 -->
|
||||
<Button
|
||||
v-if="['PENDING', 'ACCEPTED', 'IN_PROGRESS'].includes(record.status)"
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
@click="handleOpenCancel(record.id)"
|
||||
>
|
||||
<CloseCircleOutlined /> 取消
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 未选择项目提示 -->
|
||||
<a-empty v-if="!queryParams.projectId && tableData.length === 0" description="请先选择项目" />
|
||||
</div>
|
||||
|
||||
<!-- 取消任务模态框 -->
|
||||
<Modal
|
||||
v-model:open="cancelModalVisible"
|
||||
title="取消任务"
|
||||
@ok="handleCancel"
|
||||
>
|
||||
<p>确定要取消该维保任务吗?</p>
|
||||
</Modal>
|
||||
|
||||
<!-- 完成任务模态框 -->
|
||||
<Modal
|
||||
v-model:open="completeModalVisible"
|
||||
title="完成任务"
|
||||
@ok="handleComplete"
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="完成备注">
|
||||
<Input.TextArea
|
||||
v-model:value="completionNotes"
|
||||
placeholder="请输入完成备注"
|
||||
:rows="4"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</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: 24px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
<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 } from 'ant-design-vue'
|
||||
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 type { ColumnsType } from 'ant-design-vue/es/table'
|
||||
import {
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
ReloadOutlined
|
||||
ReloadOutlined,
|
||||
UserAddOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import {
|
||||
queryProjects,
|
||||
|
|
@ -14,7 +15,8 @@ import {
|
|||
updateProject,
|
||||
deleteProject,
|
||||
enableProject,
|
||||
disableProject
|
||||
disableProject,
|
||||
getProjectStatistics
|
||||
} from '@/api/project'
|
||||
import { TableActions, Pagination, StatusTag } from '@/components'
|
||||
import type { Project } from '@/types'
|
||||
|
|
@ -61,6 +63,14 @@ const typeOptions = Object.entries(ProjectTypeMap).map(([value, { label }]) => (
|
|||
label
|
||||
}))
|
||||
|
||||
// 成员表格列
|
||||
const memberColumns: ColumnsType = [
|
||||
{ title: '姓名', dataIndex: 'realName', key: 'realName' },
|
||||
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
||||
{ title: '角色', dataIndex: 'roleInProject', key: 'roleInProject' },
|
||||
{ title: '加入时间', dataIndex: 'createdAt', key: 'createdAt' }
|
||||
]
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive<ProjectQuery>({
|
||||
keyword: '',
|
||||
|
|
@ -99,6 +109,35 @@ const viewStatistics = ref<any>(null)
|
|||
const viewMembers = ref<any[]>([])
|
||||
const viewActiveTab = ref('info')
|
||||
|
||||
// 编辑抽屉状态
|
||||
const editDrawerVisible = ref(false)
|
||||
const editProject = ref<Project | null>(null)
|
||||
const editLoading = ref(false)
|
||||
const editActiveTab = ref('info')
|
||||
const editFormState = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
address: '',
|
||||
projectType: 'RESIDENTIAL' as ProjectType,
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
status: 'ACTIVE'
|
||||
})
|
||||
const editSubmitting = ref(false)
|
||||
|
||||
// 新增表单状态
|
||||
const createFormState = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
address: '',
|
||||
projectType: 'RESIDENTIAL' as ProjectType,
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
status: 'ACTIVE'
|
||||
})
|
||||
|
||||
// 点击项目名称查看详情
|
||||
const handleNameClick = async (record: Project) => {
|
||||
viewProject.value = record
|
||||
|
|
@ -118,17 +157,13 @@ const fetchViewData = async (id: string) => {
|
|||
viewLoading.value = false
|
||||
}
|
||||
}
|
||||
const formState = ref<ProjectFormData>({
|
||||
id: '',
|
||||
name: '',
|
||||
description: '',
|
||||
address: '',
|
||||
projectType: 'RESIDENTIAL',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
status: 'ACTIVE'
|
||||
})
|
||||
|
||||
// 编辑项目
|
||||
const handleEdit = async (record: Project) => {
|
||||
editProject.value = record
|
||||
editActiveTab.value = 'info'
|
||||
editDrawerVisible.value = true
|
||||
}
|
||||
|
||||
// 获取项目列表
|
||||
const fetchProjects = async () => {
|
||||
|
|
@ -173,9 +208,7 @@ const handlePageChange = (page: number, pageSize: number) => {
|
|||
|
||||
// 新增
|
||||
const handleAdd = async () => {
|
||||
drawerTitle.value = '新增项目'
|
||||
formState.value = {
|
||||
id: '',
|
||||
createFormState.value = {
|
||||
name: '',
|
||||
description: '',
|
||||
address: '',
|
||||
|
|
@ -188,26 +221,29 @@ const handleAdd = async () => {
|
|||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (record: Project) => {
|
||||
drawerTitle.value = '编辑项目'
|
||||
formState.value = {
|
||||
id: record.id,
|
||||
// 快速编辑项目
|
||||
const handleQuickEdit = async (record: Project) => {
|
||||
editProject.value = record
|
||||
editActiveTab.value = 'info'
|
||||
editFormState.value = {
|
||||
name: record.name,
|
||||
description: record.description || '',
|
||||
address: record.address || '',
|
||||
projectType: record.projectType,
|
||||
projectType: record.projectType || 'RESIDENTIAL',
|
||||
province: record.province || '',
|
||||
city: record.city || '',
|
||||
district: record.district || '',
|
||||
status: record.status
|
||||
status: record.status || 'ACTIVE'
|
||||
}
|
||||
drawerVisible.value = true
|
||||
editDrawerVisible.value = true
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleView = (record: Project) => {
|
||||
router.push(`/project/detail/${record.id}`)
|
||||
const handleViewProject = async (record: Project) => {
|
||||
viewProject.value = record
|
||||
viewActiveTab.value = 'info'
|
||||
viewDrawerVisible.value = true
|
||||
await fetchViewData(record.id)
|
||||
}
|
||||
|
||||
// 成员管理
|
||||
|
|
@ -248,32 +284,32 @@ const handleDelete = async (id: string) => {
|
|||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
const handleCreateSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
submitting.value = true
|
||||
|
||||
if (formState.value.id) {
|
||||
await updateProject(formState.value.id, formState.value)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await createProject(formState.value)
|
||||
message.success('创建成功')
|
||||
}
|
||||
await createProject(createFormState.value)
|
||||
message.success('创建成功')
|
||||
drawerVisible.value = false
|
||||
fetchProjects()
|
||||
} catch (error: any) {
|
||||
if (error.errorFields) return
|
||||
message.error('操作失败')
|
||||
message.error('创建失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭抽屉
|
||||
const handleClose = () => {
|
||||
formRef.value?.resetFields()
|
||||
drawerVisible.value = false
|
||||
const handleEditSubmit = async () => {
|
||||
try {
|
||||
editSubmitting.value = true
|
||||
await updateProject(editProject.value!.id, editFormState.value)
|
||||
message.success('更新成功')
|
||||
editDrawerVisible.value = false
|
||||
fetchProjects()
|
||||
} catch (error: any) {
|
||||
message.error('更新失败')
|
||||
} finally {
|
||||
editSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 状态标签配置
|
||||
|
|
@ -401,51 +437,132 @@ onMounted(fetchProjects)
|
|||
/>
|
||||
</div>
|
||||
|
||||
<!-- 抽屉 -->
|
||||
<!-- 新增抽屉 -->
|
||||
<Drawer
|
||||
v-model:open="drawerVisible"
|
||||
:title="drawerTitle"
|
||||
title="新增项目"
|
||||
width="560px"
|
||||
:footer-style="{ textAlign: 'right' }"
|
||||
@close="handleClose"
|
||||
@close="drawerVisible = false"
|
||||
>
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
:model="createFormState"
|
||||
layout="vertical"
|
||||
:rules="{
|
||||
name: [{ required: true, message: '请输入项目名称' }]
|
||||
}"
|
||||
>
|
||||
<Form.Item label="项目名称" name="name">
|
||||
<Input v-model:value="formState.name" placeholder="请输入项目名称" />
|
||||
<Input v-model:value="createFormState.name" placeholder="请输入项目名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label="项目类型" name="projectType">
|
||||
<Select v-model:value="formState.projectType" placeholder="请选择项目类型" :options="typeOptions" />
|
||||
<Select v-model:value="createFormState.projectType" placeholder="请选择项目类型" :options="typeOptions" />
|
||||
</Form.Item>
|
||||
<Form.Item label="描述" name="description">
|
||||
<Input.TextArea v-model:value="formState.description" placeholder="请输入描述" :rows="2" />
|
||||
<Input.TextArea v-model:value="createFormState.description" placeholder="请输入描述" :rows="2" />
|
||||
</Form.Item>
|
||||
<Form.Item label="省份" name="province">
|
||||
<Input v-model:value="formState.province" placeholder="请输入省份" />
|
||||
<Input v-model:value="createFormState.province" placeholder="请输入省份" />
|
||||
</Form.Item>
|
||||
<Form.Item label="城市" name="city">
|
||||
<Input v-model:value="formState.city" placeholder="请输入城市" />
|
||||
<Input v-model:value="createFormState.city" placeholder="请输入城市" />
|
||||
</Form.Item>
|
||||
<Form.Item label="区县" name="district">
|
||||
<Input v-model:value="formState.district" placeholder="请输入区县" />
|
||||
<Input v-model:value="createFormState.district" placeholder="请输入区县" />
|
||||
</Form.Item>
|
||||
<Form.Item label="详细地址" name="address">
|
||||
<Input v-model:value="formState.address" placeholder="请输入详细地址" />
|
||||
<Input v-model:value="createFormState.address" placeholder="请输入详细地址" />
|
||||
</Form.Item>
|
||||
<Form.Item label="状态" name="status">
|
||||
<Select v-model:value="formState.status" :options="statusOptions" />
|
||||
<Select v-model:value="createFormState.status" :options="statusOptions" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="handleClose">取消</Button>
|
||||
<Button type="primary" :loading="submitting" @click="handleSubmit">确定</Button>
|
||||
<Button @click="drawerVisible = false">取消</Button>
|
||||
<Button type="primary" :loading="submitting" @click="handleCreateSubmit">确定</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</Drawer>
|
||||
|
||||
<!-- 编辑抽屉 -->
|
||||
<Drawer
|
||||
v-model:open="editDrawerVisible"
|
||||
title="编辑项目"
|
||||
width="900px"
|
||||
:destroyOnClose="true"
|
||||
>
|
||||
<template v-if="editProject">
|
||||
<Tabs v-model:activeKey="editActiveTab">
|
||||
<TabPane key="info" tab="基本信息">
|
||||
<Form
|
||||
:model="editFormState"
|
||||
layout="vertical"
|
||||
:rules="{
|
||||
name: [{ required: true, message: '请输入项目名称' }]
|
||||
}"
|
||||
>
|
||||
<Row :gutter="16">
|
||||
<Col :span="12">
|
||||
<Form.Item label="项目名称" name="name">
|
||||
<Input v-model:value="editFormState.name" placeholder="请输入项目名称" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Form.Item label="项目类型" name="projectType">
|
||||
<Select v-model:value="editFormState.projectType" :options="typeOptions" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Form.Item label="省份" name="province">
|
||||
<Input v-model:value="editFormState.province" placeholder="请输入省份" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Form.Item label="城市" name="city">
|
||||
<Input v-model:value="editFormState.city" placeholder="请输入城市" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Form.Item label="区县" name="district">
|
||||
<Input v-model:value="editFormState.district" placeholder="请输入区县" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="12">
|
||||
<Form.Item label="状态" name="status">
|
||||
<Select v-model:value="editFormState.status" :options="statusOptions" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="24">
|
||||
<Form.Item label="详细地址" name="address">
|
||||
<Input.TextArea v-model:value="editFormState.address" placeholder="请输入详细地址" :rows="2" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col :span="24">
|
||||
<Form.Item label="描述" name="description">
|
||||
<Input.TextArea v-model:value="editFormState.description" placeholder="请输入描述" :rows="3" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</TabPane>
|
||||
<TabPane key="members" tab="成员管理">
|
||||
<div style="text-align: right; margin-bottom: 8px">
|
||||
<Button type="primary" size="small">
|
||||
<UserAddOutlined /> 添加成员
|
||||
</Button>
|
||||
</div>
|
||||
<Table :columns="memberColumns" :dataSource="[]" :pagination="false" size="small" />
|
||||
</TabPane>
|
||||
<TabPane key="config" tab="功能配置">
|
||||
<Empty description="暂无配置数据" />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</template>
|
||||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="editDrawerVisible = false">取消</Button>
|
||||
<Button type="primary" :loading="editSubmitting" @click="handleEditSubmit">确定</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</Drawer>
|
||||
|
|
@ -453,45 +570,83 @@ onMounted(fetchProjects)
|
|||
<Drawer
|
||||
v-model:open="viewDrawerVisible"
|
||||
title="项目详情"
|
||||
width="600px"
|
||||
width="900px"
|
||||
:destroyOnClose="true"
|
||||
>
|
||||
<template v-if="viewProject">
|
||||
<Descriptions :column="2" bordered size="small">
|
||||
<DescriptionsItem label="项目名称">{{ viewProject.name }}</DescriptionsItem>
|
||||
<DescriptionsItem label="项目类型">
|
||||
<a-tag :color="ProjectTypeMap[viewProject.projectType as ProjectType]?.color">
|
||||
{{ ProjectTypeMap[viewProject.projectType as ProjectType]?.label || '-' }}
|
||||
</a-tag>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="状态">
|
||||
<a-tag :color="ProjectStatusMap[viewProject.status as ProjectStatus]?.color">
|
||||
{{ ProjectStatusMap[viewProject.status as ProjectStatus]?.label || '-' }}
|
||||
</a-tag>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="描述" :span="2">{{ viewProject.description || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="省份">{{ viewProject.province || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="城市">{{ viewProject.city || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="区县">{{ viewProject.district || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="详细地址" :span="2">{{ viewProject.address || '-' }}</DescriptionsItem>
|
||||
</Descriptions>
|
||||
<Divider />
|
||||
<Row :gutter="16">
|
||||
<Col :span="8">
|
||||
<Row :gutter="16" class="statistics-row">
|
||||
<Col :span="4">
|
||||
<Card size="small">
|
||||
<Statistic title="楼栋数" :value="viewProject.buildingCount || 0" />
|
||||
<Statistic title="成员数" :value="viewStatistics?.memberCount || 0" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Col :span="4">
|
||||
<Card size="small">
|
||||
<Statistic title="单元数" :value="viewProject.unitCount || 0" />
|
||||
<Statistic title="楼栋数" :value="viewStatistics?.buildingCount || 0" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="8">
|
||||
<Col :span="4">
|
||||
<Card size="small">
|
||||
<Statistic title="房间数" :value="viewProject.roomCount || 0" />
|
||||
<Statistic title="房间数" :value="viewStatistics?.roomCount || 0" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="4">
|
||||
<Card size="small">
|
||||
<Statistic title="业主数" :value="viewStatistics?.ownerCount || 0" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="4">
|
||||
<Card size="small">
|
||||
<Statistic title="租户数" :value="viewStatistics?.tenantCount || 0" />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col :span="4">
|
||||
<Card size="small">
|
||||
<Statistic title="进行中任务" :value="viewStatistics?.activeTaskCount || 0" />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Tabs v-model:activeKey="viewActiveTab" style="margin-top: 16px">
|
||||
<TabPane key="info" tab="基本信息">
|
||||
<Descriptions :column="2" bordered size="small">
|
||||
<DescriptionsItem label="项目名称">{{ viewProject.name }}</DescriptionsItem>
|
||||
<DescriptionsItem label="项目类型">
|
||||
<a-tag :color="ProjectTypeMap[viewProject.projectType as ProjectType]?.color">
|
||||
{{ ProjectTypeMap[viewProject.projectType as ProjectType]?.label || '-' }}
|
||||
</a-tag>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="状态">
|
||||
<a-tag :color="ProjectStatusMap[viewProject.status as ProjectStatus]?.color">
|
||||
{{ ProjectStatusMap[viewProject.status as ProjectStatus]?.label || '-' }}
|
||||
</a-tag>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="描述" :span="2">{{ viewProject.description || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="省份">{{ viewProject.province || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="城市">{{ viewProject.city || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="区县">{{ viewProject.district || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="详细地址" :span="2">{{ viewProject.address || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="创建时间">{{ viewProject.createdAt || '-' }}</DescriptionsItem>
|
||||
<DescriptionsItem label="更新时间">{{ viewProject.updatedAt || '-' }}</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</TabPane>
|
||||
<TabPane key="members" tab="成员管理">
|
||||
<div style="text-align: right; margin-bottom: 8px">
|
||||
<Button type="primary" size="small">
|
||||
<UserAddOutlined /> 添加成员
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
:columns="memberColumns"
|
||||
:dataSource="viewMembers"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane key="config" tab="功能配置">
|
||||
<Empty description="暂无配置数据" />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</template>
|
||||
</Drawer>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,10 +8,16 @@ 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,
|
||||
Pagination
|
||||
} from '@/components'
|
||||
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{ title: '时间', dataIndex: 'createdAt', key: 'createdAt', width: 170 },
|
||||
{ title: '操作用户', dataIndex: 'username', key: 'username', width: 100 },
|
||||
|
|
@ -23,7 +29,6 @@ const columns = [
|
|||
{ title: '耗时', dataIndex: 'executionTimeMs', key: 'executionTimeMs', width: 80 }
|
||||
]
|
||||
|
||||
// 数据
|
||||
const logs = ref<AuditLog[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = ref({
|
||||
|
|
@ -32,17 +37,14 @@ const pagination = ref({
|
|||
total: 0
|
||||
})
|
||||
|
||||
// 统计数据
|
||||
const stats = ref({
|
||||
total: 0,
|
||||
retentionDays: 30
|
||||
})
|
||||
|
||||
// 筛选选项
|
||||
const moduleOptions = ref<{ value: string; label: string }[]>([])
|
||||
const actionOptions = ref<{ value: string; label: string }[]>([])
|
||||
|
||||
// 筛选条件
|
||||
const filters = ref({
|
||||
module: undefined as string | undefined,
|
||||
action: undefined as string | undefined,
|
||||
|
|
@ -50,13 +52,11 @@ const filters = ref({
|
|||
dateRange: null as [Dayjs, Dayjs] | null
|
||||
})
|
||||
|
||||
// 加载模块选项
|
||||
const loadModules = async () => {
|
||||
try {
|
||||
const res = await getAuditModules()
|
||||
moduleOptions.value = res.data.data || []
|
||||
} catch {
|
||||
// 使用默认值
|
||||
moduleOptions.value = [
|
||||
{ value: 'USER', label: '用户管理' },
|
||||
{ value: 'ROLE', label: '角色管理' },
|
||||
|
|
@ -66,13 +66,11 @@ const loadModules = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 加载操作类型选项
|
||||
const loadActions = async () => {
|
||||
try {
|
||||
const res = await getAuditActions()
|
||||
actionOptions.value = res.data.data || []
|
||||
} catch {
|
||||
// 使用默认值
|
||||
actionOptions.value = [
|
||||
{ value: 'CREATE', label: '创建' },
|
||||
{ value: 'UPDATE', label: '修改' },
|
||||
|
|
@ -84,17 +82,15 @@ const loadActions = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 加载统计数据
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const res = await getAuditStats()
|
||||
stats.value = res.data.data || { total: 0, retentionDays: 30 }
|
||||
} catch {
|
||||
// 忽略错误
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
|
|
@ -130,20 +126,17 @@ const loadData = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleTableChange = (pag: any) => {
|
||||
pagination.value.current = pag.current
|
||||
pagination.value.pageSize = pag.pageSize
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 查询
|
||||
const handleSearch = () => {
|
||||
pagination.value.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
filters.value = {
|
||||
module: undefined,
|
||||
|
|
@ -155,7 +148,6 @@ const handleReset = () => {
|
|||
loadData()
|
||||
}
|
||||
|
||||
// 获取模块标签
|
||||
const getModuleLabel = (module: string) => {
|
||||
const map: Record<string, string> = {
|
||||
USER: '用户管理',
|
||||
|
|
@ -167,13 +159,13 @@ const getModuleLabel = (module: string) => {
|
|||
return map[module] || module
|
||||
}
|
||||
|
||||
// 获取操作类型标签
|
||||
const getActionLabel = (action: string) => {
|
||||
const map: Record<string, string> = {
|
||||
CREATE: '创建',
|
||||
UPDATE: '修改',
|
||||
DELETE: '删除',
|
||||
QUERY: '查询',
|
||||
VIEW: '查看',
|
||||
LOGIN: '登录',
|
||||
LOGOUT: '登出',
|
||||
EXPORT: '导出',
|
||||
|
|
@ -184,13 +176,13 @@ const getActionLabel = (action: string) => {
|
|||
return map[action] || action
|
||||
}
|
||||
|
||||
// 获取操作类型颜色
|
||||
const getActionColor = (action: string) => {
|
||||
const map: Record<string, string> = {
|
||||
CREATE: 'green',
|
||||
UPDATE: 'blue',
|
||||
DELETE: 'red',
|
||||
QUERY: 'default',
|
||||
VIEW: 'cyan',
|
||||
LOGIN: 'cyan',
|
||||
LOGOUT: 'default',
|
||||
EXPORT: 'purple',
|
||||
|
|
@ -201,24 +193,20 @@ const getActionColor = (action: string) => {
|
|||
return map[action] || 'default'
|
||||
}
|
||||
|
||||
// 获取状态标签
|
||||
const getStatusLabel = (status: string) => {
|
||||
return status === 'SUCCESS' ? '成功' : '失败'
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
return status === 'SUCCESS' ? 'success' : 'error'
|
||||
}
|
||||
|
||||
// 格式化耗时
|
||||
const formatDuration = (ms?: number) => {
|
||||
if (ms === undefined || ms === null) return '-'
|
||||
if (ms < 1000) return `${ms}ms`
|
||||
return `${(ms / 1000).toFixed(2)}s`
|
||||
}
|
||||
|
||||
// 禁用30天前的日期
|
||||
const disabledDate = (current: Dayjs) => {
|
||||
const thirtyDaysAgo = dayjs().subtract(30, 'day').startOf('day')
|
||||
return current && (current < thirtyDaysAgo || current > dayjs().endOf('day'))
|
||||
|
|
@ -235,16 +223,16 @@ onMounted(() => {
|
|||
<template>
|
||||
<ConfigProvider :locale="zhCN">
|
||||
<div class="page-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">操作审计日志</h2>
|
||||
<div class="page-subtitle">
|
||||
保留最近 {{ stats.retentionDays }} 天的操作记录,共 {{ stats.total }} 条
|
||||
</div>
|
||||
</div>
|
||||
<PageHeader>
|
||||
<template #title>
|
||||
<span>操作审计日志</span>
|
||||
</template>
|
||||
<template #subtitle>
|
||||
<span>保留最近 {{ stats.retentionDays }} 天的操作记录,共 {{ stats.total }} 条</span>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-bar">
|
||||
<FilterBar>
|
||||
<Space wrap>
|
||||
<Select
|
||||
v-model:value="filters.module"
|
||||
|
|
@ -281,11 +269,12 @@ onMounted(() => {
|
|||
<ReloadOutlined /> 重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</FilterBar>
|
||||
|
||||
<!-- 表格 -->
|
||||
<div class="table-card">
|
||||
<Table
|
||||
<TableCard>
|
||||
<TableToolbar @refresh="loadData" />
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="logs"
|
||||
:loading="loading"
|
||||
|
|
@ -320,35 +309,15 @@ onMounted(() => {
|
|||
{{ dayjs(record.createdAt).format('YYYY-MM-DD HH:mm:ss') }}
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
</a-table>
|
||||
|
||||
<Pagination
|
||||
v-model:current="pagination.current"
|
||||
v-model:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@change="handleTableChange"
|
||||
/>
|
||||
</TableCard>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,24 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Card, Form, FormItem, Input, Button, message, Breadcrumb, BreadcrumbItem } from 'ant-design-vue'
|
||||
import { HomeOutlined } from '@ant-design/icons-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
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 router = useRouter()
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const formRef = ref()
|
||||
|
||||
const formState = ref({
|
||||
propertyCompanyName: '',
|
||||
propertyCompanyAddress: '',
|
||||
propertyCompanyPhone: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
propertyCompanyName: [{ required: true, message: '请输入物业企业名称' }]
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
|
|
@ -30,6 +35,12 @@ onMounted(async () => {
|
|||
})
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
await updateConfig({
|
||||
|
|
@ -48,44 +59,55 @@ const handleSubmit = async () => {
|
|||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbItem>
|
||||
<HomeOutlined />
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbItem>系统管理</BreadcrumbItem>
|
||||
<BreadcrumbItem>系统设置</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">系统设置</h2>
|
||||
</div>
|
||||
<PageHeader>
|
||||
<template #title>
|
||||
<span>系统设置</span>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<Card :loading="loading">
|
||||
<Form layout="vertical">
|
||||
<FormItem label="物业企业名称">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
layout="vertical"
|
||||
:rules="rules"
|
||||
>
|
||||
<Divider orientation="left">基本信息</Divider>
|
||||
|
||||
<FormItem label="物业企业名称" name="propertyCompanyName">
|
||||
<Input
|
||||
v-model:value="formState.propertyCompanyName"
|
||||
placeholder="请输入物业企业名称"
|
||||
:maxlength="100"
|
||||
style="width: 400px"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="物业企业地址">
|
||||
|
||||
<FormItem label="物业企业地址" name="propertyCompanyAddress">
|
||||
<Input
|
||||
v-model:value="formState.propertyCompanyAddress"
|
||||
placeholder="请输入物业企业地址"
|
||||
:maxlength="200"
|
||||
style="width: 600px"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="物业企业电话">
|
||||
|
||||
<FormItem label="物业企业电话" name="propertyCompanyPhone">
|
||||
<Input
|
||||
v-model:value="formState.propertyCompanyPhone"
|
||||
placeholder="请输入物业企业电话"
|
||||
:maxlength="20"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<Button type="primary" :loading="submitting" @click="handleSubmit">
|
||||
保存设置
|
||||
<Button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<SaveOutlined /> 保存设置
|
||||
</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
|
|
|
|||
Loading…
Reference in New Issue