feat: add maintenance management frontend pages

This commit is contained in:
chiguyong 2026-03-24 00:19:06 +08:00
parent 7956379f71
commit 913d6400e4
9 changed files with 1305 additions and 174 deletions

156
src/api/maintenance.ts Normal file
View File

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

View File

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

View File

@ -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: '维保任务' }
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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