Add delete functionality for patrol events, tasks, and plans

- Implement single and batch delete methods for events, tasks, and plans
- Add delete API endpoints in respective modules
- Update Vue components with delete buttons and confirmation dialogs
- Implement validation to prevent deleting tasks/plans with active status or related records
- Enhance user experience with clear error messages and selective deletion
This commit is contained in:
wzclm 2025-02-23 15:30:58 +08:00
parent 1972b43dec
commit 90107b33d3
13 changed files with 1580 additions and 179 deletions

View File

@ -0,0 +1,40 @@
import request from '@/utils/request'
// 查询观察记录列表
export function getObservationList(query) {
return request.get('/api/admin/observations', { params: query })
}
// 获取观察记录详细信息
export function getObservation(id) {
return request.get(`/api/admin/observations/${id}`)
}
// 创建观察记录
export function createObservation(data) {
return request.post('/api/admin/observations', data)
}
// 更新观察记录
export function updateObservation(id, data) {
return request.put(`/api/admin/observations/${id}`, data)
}
// 更新观察记录状态(审核)
export function reviewObservation(id, data) {
return request.put(`/api/admin/observations/${id}/status`, data)
}
// 删除观察记录
export function deleteObservation(id) {
return request.delete(`/api/admin/observations/${id}`)
}
// 导出观察记录
export function exportObservation(query) {
return request({
url: '/monitor/observation/export',
method: 'get',
params: query
})
}

View File

@ -114,4 +114,22 @@ export function batchExportEvents(ids) {
*/
export function getEventStatistics() {
return request.get('/api/admin/patrol/events/statistics/overview')
}
/**
* 删除单个事件
* @param {string|number} id - 事件ID
* @returns {Promise} 返回删除结果
*/
export function deleteEvent(id) {
return request.delete(`/api/admin/patrol/events/${id}`)
}
/**
* 批量删除事件
* @param {Array<string|number>} ids - 事件ID列表
* @returns {Promise} 返回批量删除结果
*/
export function batchDeleteEvents(ids) {
return request.post('/api/admin/patrol/events/batch/delete', { ids })
}

View File

@ -91,6 +91,13 @@ export function checkPlanName(name, excludeId) {
/**
* 删除巡护计划
* 只能删除以下状态的计划
* - 未开始的计划
* - 已完成的计划
* - 已取消的计划
* 不能删除
* - 进行中的计划
* - 已有关联巡护任务的计划
* @param {string|number} id - 计划ID
* @returns {Promise} 返回删除结果
*/
@ -100,6 +107,13 @@ export function deletePlan(id) {
/**
* 批量删除巡护计划
* 只能删除以下状态的计划
* - 未开始的计划
* - 已完成的计划
* - 已取消的计划
* 不能删除
* - 进行中的计划
* - 已有关联巡护任务的计划
* @param {Array<string|number>} ids - 计划ID列表
* @returns {Promise} 返回批量删除结果
*/

View File

@ -90,4 +90,36 @@ export function getExecutorTasks(executorId, params = {}) {
*/
export function cancelTask(id, data = {}) {
return request.post(`/api/admin/patrol/tasks/${id}/cancel`, data)
}
/**
* 删除巡护任务
* 只能删除以下状态的任务
* - 未开始的任务
* - 已完成的任务
* - 已取消的任务
* 不能删除
* - 进行中的任务
* - 已有巡护记录的任务
* @param {string|number} id - 任务ID
* @returns {Promise} 返回删除结果
*/
export function deleteTask(id) {
return request.delete(`/api/admin/patrol/tasks/${id}`)
}
/**
* 批量删除巡护任务
* 只能删除以下状态的任务
* - 未开始的任务
* - 已完成的任务
* - 已取消的任务
* 不能删除
* - 进行中的任务
* - 已有巡护记录的任务
* @param {Array<string|number>} ids - 任务ID列表
* @returns {Promise} 返回批量删除结果
*/
export function batchDeleteTasks(ids) {
return request.post('/api/admin/patrol/tasks/batch/delete', { ids })
}

View File

@ -124,6 +124,7 @@ const handleLogout = () => {
</template>
<el-menu-item index="/monitor/species">物种监测</el-menu-item>
<el-menu-item index="/monitor/environment">环境监测</el-menu-item>
<el-menu-item index="/monitor/observations">观测管理</el-menu-item>
</el-sub-menu>
<el-sub-menu index="patrol">

View File

@ -59,6 +59,11 @@ const router = createRouter({
name: 'EnvironmentMonitor',
component: () => import('../views/monitor/environment/index.vue')
},
{
path: 'monitor/observations',
name: 'ObservationsMonitor',
component: () => import('../views/monitor/observations/index.vue')
},
{
path: 'patrol/tasks',
name: 'PatrolTasks',

View File

@ -452,8 +452,8 @@ onMounted(() => {
</el-form-item>
<el-form-item label="导出格式">
<el-radio-group v-model="exportForm.format">
<el-radio label="excel">Excel</el-radio>
<el-radio label="csv">CSV</el-radio>
<el-radio value="excel">Excel</el-radio>
<el-radio value="csv">CSV</el-radio>
</el-radio-group>
</el-form-item>
</el-form>

View File

@ -0,0 +1,485 @@
<template>
<div class="app-container">
<!-- 搜索条件 -->
<el-card class="filter-container">
<el-form :inline="true" :model="queryParams" class="demo-form-inline">
<el-form-item label="记录标题">
<el-input v-model="queryParams.title" placeholder="请输入记录标题" clearable style="width: 240px" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 240px">
<el-option label="待审核" :value="0" />
<el-option label="已通过" :value="1" />
<el-option label="已拒绝" :value="2" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 数据列表 -->
<el-card class="list-container">
<el-table
v-loading="loading"
:data="observationList"
style="width: 100%"
border
>
<el-table-column type="index" label="序号" width="80" align="center">
<template #default="scope">
{{ (queryParams.page - 1) * queryParams.page_size + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="title" label="记录标题" min-width="150" show-overflow-tooltip align="center"/>
<el-table-column label="记录人" width="120" align="center">
<template #default="{ row }">
{{ row.user_real_name }}
</template>
</el-table-column>
<el-table-column label="物种" width="120" align="center" show-overflow-tooltip>
<template #default="{ row }">
<el-tooltip :content="row.species_latin_name" placement="top">
<span>{{ row.species_name }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="图片" width="100" align="center">
<template #default="{ row }">
<el-image
v-if="row.image_urls && row.image_urls.length > 0"
:src="row.image_urls[0]"
:preview-src-list="row.image_urls"
fit="cover"
style="width: 50px; height: 50px"
/>
</template>
</el-table-column>
<el-table-column label="位置" width="150" show-overflow-tooltip align="center">
<template #default="{ row }">
<el-tooltip :content="'经度:' + row.longitude + ', 纬度:' + row.latitude" placement="top">
<span>{{ row.location_description }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="weather" label="天气" width="100" align="center" />
<el-table-column prop="temperature" label="温度" width="100" align="center">
<template #default="{ row }">
{{ row.temperature }}°C
</template>
</el-table-column>
<el-table-column label="状态" width="120" align="center">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
<br v-if="row.reviewer_real_name" />
<small v-if="row.reviewer_real_name">审核人: {{ row.reviewer_real_name }}</small>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="180" align="center">
<template #default="{ row }">
{{ formatDateTime(row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<el-button
type="primary" link
@click="handleView(row)"
>查看</el-button>
<el-button
v-if="row.status === 0"
type="warning" link
@click="handleReview(row)"
>审核</el-button>
<el-button
type="danger" link
@click="handleDelete(row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 分页 -->
<div class="fixed-pagination">
<el-pagination
v-show="total > queryParams.page_size"
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.page_size"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 查看对话框 -->
<el-dialog
v-model="viewDialogVisible"
title="查看观察记录"
width="800px"
append-to-body
>
<el-descriptions :column="2" border>
<el-descriptions-item label="记录标题">{{ viewForm.title }}</el-descriptions-item>
<el-descriptions-item label="物种">{{ getSpeciesName(viewForm.species_id) }}</el-descriptions-item>
<el-descriptions-item label="位置描述">{{ viewForm.location_description }}</el-descriptions-item>
<el-descriptions-item label="经纬度">{{ viewForm.longitude }}, {{ viewForm.latitude }}</el-descriptions-item>
<el-descriptions-item label="天气">{{ viewForm.weather }}</el-descriptions-item>
<el-descriptions-item label="温度">{{ viewForm.temperature }}°C</el-descriptions-item>
<el-descriptions-item label="记录内容" :span="2">{{ viewForm.content }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusType(viewForm.status)">{{ getStatusText(viewForm.status) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="审核信息" v-if="viewForm.status !== 0">
<div>审核人: {{ viewForm.reviewer_real_name }}</div>
<div>审核意见: {{ viewForm.review_comment }}</div>
<div>审核时间: {{ formatDateTime(viewForm.review_time) }}</div>
</el-descriptions-item>
<el-descriptions-item label="图片" :span="2" v-if="viewForm.image_urls && viewForm.image_urls.length > 0">
<el-image
v-for="(url, index) in viewForm.image_urls"
:key="index"
:src="url"
:preview-src-list="viewForm.image_urls"
fit="cover"
style="width: 100px; height: 100px; margin-right: 10px"
/>
</el-descriptions-item>
<el-descriptions-item label="视频" :span="2" v-if="viewForm.video_urls && viewForm.video_urls.length > 0">
<video
v-for="(url, index) in viewForm.video_urls"
:key="index"
:src="url"
controls
style="width: 200px; margin-right: 10px"
/>
</el-descriptions-item>
</el-descriptions>
<template #footer>
<div class="dialog-footer">
<el-button @click="viewDialogVisible = false"> </el-button>
</div>
</template>
</el-dialog>
<!-- 审核对话框 -->
<el-dialog
v-model="reviewDialogVisible"
title="审核观察记录"
width="800px"
append-to-body
>
<el-descriptions :column="2" border>
<el-descriptions-item label="记录标题">{{ reviewForm.title }}</el-descriptions-item>
<el-descriptions-item label="物种">{{ getSpeciesName(reviewForm.species_id) }}</el-descriptions-item>
<el-descriptions-item label="位置描述">{{ reviewForm.location_description }}</el-descriptions-item>
<el-descriptions-item label="经纬度">{{ reviewForm.longitude }}, {{ reviewForm.latitude }}</el-descriptions-item>
<el-descriptions-item label="天气">{{ reviewForm.weather }}</el-descriptions-item>
<el-descriptions-item label="温度">{{ reviewForm.temperature }}°C</el-descriptions-item>
<el-descriptions-item label="记录内容" :span="2">{{ reviewForm.content }}</el-descriptions-item>
<el-descriptions-item label="图片" :span="2" v-if="reviewForm.image_urls && reviewForm.image_urls.length > 0">
<el-image
v-for="(url, index) in reviewForm.image_urls"
:key="index"
:src="url"
:preview-src-list="reviewForm.image_urls"
fit="cover"
style="width: 100px; height: 100px; margin-right: 10px"
/>
</el-descriptions-item>
<el-descriptions-item label="视频" :span="2" v-if="reviewForm.video_urls && reviewForm.video_urls.length > 0">
<video
v-for="(url, index) in reviewForm.video_urls"
:key="index"
:src="url"
controls
style="width: 200px; margin-right: 10px"
/>
</el-descriptions-item>
</el-descriptions>
<el-divider>审核信息</el-divider>
<el-form ref="reviewFormRef" :model="reviewForm" :rules="reviewRules" label-width="100px">
<el-form-item label="审核结果" prop="review_status">
<el-radio-group v-model="reviewForm.review_status">
<el-radio :value="1">通过</el-radio>
<el-radio :value="2">拒绝</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核意见" prop="review_comment">
<el-input
v-model="reviewForm.review_comment"
type="textarea"
placeholder="请输入审核意见"
:rows="3"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="reviewDialogVisible = false"> </el-button>
<el-button type="primary" @click="submitReview"> </el-button>
</div>
</template>
</el-dialog>
<!-- 图片预览 -->
<el-dialog v-model="picturePreviewVisible" append-to-body>
<img w-full :src="picturePreviewUrl" alt="Preview Image" />
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getObservationList, reviewObservation, deleteObservation } from '@/api/monitor/observation'
import { formatDateTime } from '@/utils/format'
import { sortArrayByField } from '@/utils/sort'
//
const queryParams = reactive({
page: 1,
page_size: 10,
title: '',
status: undefined
})
//
const loading = ref(false)
const observationList = ref([])
const total = ref(0)
//
const speciesList = ref([
{ id: 1, name: '东方白鹳' },
//
])
//
const picturePreviewVisible = ref(false)
const picturePreviewUrl = ref('')
//
const viewDialogVisible = ref(false)
const viewForm = ref({})
//
const reviewDialogVisible = ref(false)
const reviewFormRef = ref()
const reviewForm = ref({})
//
const reviewRules = {
review_status: [{ required: true, message: '请选择审核结果', trigger: 'change' }],
review_comment: [{ required: true, message: '请输入审核意见', trigger: 'blur' }]
}
//
const getList = async () => {
loading.value = true
try {
//
const params = {
page: queryParams.page,
page_size: queryParams.page_size
}
if (queryParams.title) {
params.title = queryParams.title
}
if (queryParams.status !== undefined) {
params.status = queryParams.status
}
const res = await getObservationList(params)
if (res.success) {
//
observationList.value = sortArrayByField(res.data.list, 'created_at', 'asc')
total.value = res.data.pagination.total
} else {
ElMessage.error(res.message || '获取数据失败')
}
} catch (error) {
console.error('获取观察记录列表失败:', error)
ElMessage.error('获取数据失败')
}
loading.value = false
}
//
const handleQuery = () => {
queryParams.page = 1
getList()
}
//
const resetQuery = () => {
queryParams.title = ''
queryParams.status = undefined
queryParams.page = 1
queryParams.page_size = 10
getList()
}
//
const handleDelete = (row) => {
ElMessageBox.confirm(
`确认要删除观察记录"${row.title}"吗?`,
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
try {
await deleteObservation(row.id)
ElMessage.success('删除成功')
//
if (observationList.value.length === 1 && queryParams.page > 1) {
queryParams.page--
}
getList()
} catch (error) {
console.error('删除失败:', error)
ElMessage.error('删除失败')
}
}).catch(() => {
//
})
}
//
const getStatusType = (status) => {
const statusMap = {
0: 'warning',
1: 'success',
2: 'danger'
}
return statusMap[status]
}
const getStatusText = (status) => {
const statusMap = {
0: '待审核',
1: '已通过',
2: '已拒绝'
}
return statusMap[status]
}
//
const handleSizeChange = (val) => {
queryParams.page_size = val
getList()
}
//
const handleCurrentChange = (val) => {
queryParams.page = val
getList()
}
//
const handlePictureCardPreview = (file) => {
picturePreviewUrl.value = file.url
picturePreviewVisible.value = true
}
//
const getSpeciesName = (id) => {
const species = speciesList.value.find(item => item.id === id)
return species ? species.name : ''
}
//
const handleView = (row) => {
viewForm.value = { ...row }
viewDialogVisible.value = true
}
//
const handleReview = (row) => {
reviewForm.value = {
id: row.id,
title: row.title,
species_id: row.species_id,
content: row.content,
location_description: row.location_description,
longitude: Number(row.longitude),
latitude: Number(row.latitude),
weather: row.weather,
temperature: Number(row.temperature),
image_urls: [...(row.image_urls || [])],
video_urls: [...(row.video_urls || [])],
status: row.status,
review_status: 1,
review_comment: ''
}
reviewDialogVisible.value = true
}
//
const submitReview = async () => {
await reviewFormRef.value.validate()
try {
const formData = {
status: reviewForm.value.review_status,
review_comment: reviewForm.value.review_comment
}
await reviewObservation(reviewForm.value.id, formData)
ElMessage.success('审核成功')
reviewDialogVisible.value = false
getList()
} catch (error) {
console.error('审核失败:', error)
ElMessage.error('审核失败')
}
}
onMounted(() => {
getList()
})
</script>
<style scoped>
.filter-container {
margin-bottom: 20px;
}
.list-container {
margin-top: 20px;
margin-bottom: 60px;
}
.dialog-footer {
text-align: right;
}
.fixed-pagination {
position: fixed;
right: 20px;
bottom: 20px;
padding: 10px 20px;
background-color: white;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
border-radius: 4px;
z-index: 1000;
}
.text-center {
text-align: center;
}
.text-gray-500 {
color: #909399;
}
</style>

View File

@ -1,8 +1,8 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, Plus, Download } from '@element-plus/icons-vue'
import { getEventList, updateEventStatus, createEvent, getEventDetail, getEventStatistics, exportEvents, batchExportEvents } from '@/api/patrol/event'
import { Search, Refresh, Plus, Download, Delete } from '@element-plus/icons-vue'
import { getEventList, updateEventStatus, createEvent, getEventDetail, getEventStatistics, exportEvents, batchExportEvents, deleteEvent, batchDeleteEvents } from '@/api/patrol/event'
import { formatDateTime } from '@/utils/format'
//
@ -15,7 +15,8 @@ const statistics = ref({
pending: 0,
processing: 0,
processed: 0,
closed: 0
closed: 0,
avgHandlingTime: 0
})
//
@ -102,15 +103,15 @@ const getList = async () => {
})
if (res.success) {
tableData.value = res.data.list
//
tableData.value = [...res.data.list].reverse()
total.value = Number(res.data.pagination.total)
currentPage.value = Number(res.data.pagination.page || 1)
pageSize.value = Number(res.data.pagination.page_size || 10)
} else {
ElMessage.error(res.message || '获取列表失败')
}
} catch (error) {
console.error('获取安防事件列表错误:', error)
console.error('获取事件列表错误:', error)
ElMessage.error('获取列表失败')
} finally {
loading.value = false
@ -122,10 +123,23 @@ const getStatistics = async () => {
try {
const res = await getEventStatistics()
if (res.success) {
statistics.value = res.data
//
const statusData = res.data.status_distribution
statistics.value = {
total: res.data.total_events || 0,
pending: statusData.find(item => item.status === 0)?.count || 0,
processing: statusData.find(item => item.status === 1)?.count || 0,
processed: statusData.find(item => item.status === 2)?.count || 0,
closed: statusData.find(item => item.status === 3)?.count || 0,
avgHandlingTime: res.data.avg_handling_time?.hours || 0
}
} else {
console.error('获取统计数据失败:', res.message)
ElMessage.error('获取统计数据失败')
}
} catch (error) {
console.error('获取统计数据错误:', error)
ElMessage.error('获取统计数据失败')
}
}
@ -367,6 +381,81 @@ const handleSelectionChange = (selection) => {
selectedRows.value = selection
}
//
const hasSelectedDeletableRows = computed(() => {
return selectedRows.value.some(row => row.handling_status === 2 || row.handling_status === 3)
})
//
const handleDelete = (row) => {
if (row.handling_status === 0 || row.handling_status === 1) {
ElMessage.warning('待处理或处理中的事件不能删除')
return
}
ElMessageBox.confirm(
'确认删除该事件吗?此操作不可恢复',
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
try {
const res = await deleteEvent(row.id)
if (res.success) {
ElMessage.success('删除成功')
getList()
getStatistics()
} else {
ElMessage.error(res.message || '删除失败')
}
} catch (error) {
console.error('删除事件错误:', error)
ElMessage.error('删除失败')
}
}).catch(() => {})
}
//
const handleBatchDelete = () => {
const deletableRows = selectedRows.value.filter(
row => row.handling_status === 2 || row.handling_status === 3
)
if (deletableRows.length === 0) {
ElMessage.warning('没有可删除的事件(只能删除已处理或已关闭的事件)')
return
}
ElMessageBox.confirm(
`确认删除选中的 ${deletableRows.length} 个事件吗?此操作不可恢复`,
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
try {
const res = await batchDeleteEvents(deletableRows.map(row => row.id))
if (res.success) {
ElMessage.success('批量删除成功')
getList()
getStatistics()
//
selectedRows.value = []
} else {
ElMessage.error(res.message || '批量删除失败')
}
} catch (error) {
console.error('批量删除事件错误:', error)
ElMessage.error('批量删除失败')
}
}).catch(() => {})
}
onMounted(() => {
getList()
getStatistics()
@ -437,6 +526,7 @@ onMounted(() => {
<el-button type="primary" :icon="Plus" @click="handleCreate">新增事件</el-button>
<el-button type="success" :icon="Download" @click="handleExport">全部导出</el-button>
<el-button type="success" :icon="Download" @click="handleBatchExport" :disabled="selectedRows.length === 0">批量导出</el-button>
<el-button type="danger" :icon="Delete" @click="handleBatchDelete" :disabled="!hasSelectedDeletableRows">批量删除</el-button>
<el-button :icon="Refresh" @click="handleRefresh">刷新</el-button>
</div>
</div>
@ -568,6 +658,12 @@ onMounted(() => {
link
@click="handleClose(row)"
>关闭</el-button>
<el-button
type="danger"
link
@click="handleDelete(row)"
v-if="row.handling_status === 2 || row.handling_status === 3"
>删除</el-button>
</template>
</el-table-column>
</el-table>

View File

@ -5,7 +5,8 @@
<div class="card-header">
<span class="header-title">巡护计划管理</span>
<div class="header-btns">
<el-button type="primary" :icon="Plus" @click="newPlanDialogVisible = true">新建计划</el-button>
<el-button type="primary" :icon="Plus" @click="handleCreate">新建计划</el-button>
<el-button :icon="Delete" @click="handleBatchDelete" :disabled="!hasSelectedDeletableRows">批量删除</el-button>
<el-button :icon="Refresh" @click="handleRefresh">刷新</el-button>
</div>
</div>
@ -126,7 +127,7 @@
<el-table-column label="操作" width="200" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="primary" @click="handleView(row)">查看</el-button>
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button
link
type="primary"
@ -134,7 +135,14 @@
>
{{ row.status === 1 ? '禁用' : '启用' }}
</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
<el-button
link
type="danger"
@click="handleDelete(row)"
v-if="canDeletePlan(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
@ -155,25 +163,26 @@
</div>
</el-card>
<!-- 新建计划弹窗 -->
<!-- 巡护计划表单弹窗 -->
<el-dialog
v-model="newPlanDialogVisible"
title="新建巡护计划"
v-model="planDialogVisible"
:title="isEdit ? '编辑巡护计划' : '新建巡护计划'"
width="600px"
@close="handleDialogClose"
>
<el-form
ref="newPlanFormRef"
:model="newPlanForm"
ref="planFormRef"
:model="planForm"
:rules="rules"
label-width="100px"
class="plan-form"
>
<el-form-item label="计划名称" prop="plan_name">
<el-input v-model="newPlanForm.plan_name" placeholder="请输入计划名称" />
<el-input v-model="planForm.plan_name" placeholder="请输入计划名称" />
</el-form-item>
<el-form-item label="计划类型" prop="plan_type">
<el-radio-group v-model="newPlanForm.plan_type">
<el-radio-group v-model="planForm.plan_type">
<el-radio value="daily">日常巡护</el-radio>
<el-radio value="special">专项巡护</el-radio>
</el-radio-group>
@ -183,7 +192,7 @@
<el-col :span="11">
<el-form-item prop="start_date">
<el-date-picker
v-model="newPlanForm.start_date"
v-model="planForm.start_date"
type="date"
placeholder="开始日期"
style="width: 100%"
@ -197,7 +206,7 @@
<el-col :span="11">
<el-form-item prop="end_date">
<el-date-picker
v-model="newPlanForm.end_date"
v-model="planForm.end_date"
type="date"
placeholder="结束日期"
style="width: 100%"
@ -208,7 +217,7 @@
</el-form-item>
<el-form-item label="任务频次" prop="task_frequency">
<el-radio-group v-model="newPlanForm.task_frequency">
<el-radio-group v-model="planForm.task_frequency">
<el-radio value="daily">每日</el-radio>
<el-radio value="weekly">每周</el-radio>
<el-radio value="monthly">每月</el-radio>
@ -259,7 +268,7 @@
<el-form-item label="计划描述" prop="description">
<el-input
v-model="newPlanForm.description"
v-model="planForm.description"
type="textarea"
:rows="4"
placeholder="请输入计划描述"
@ -267,7 +276,7 @@
</el-form-item>
<el-form-item label="计划状态" prop="status">
<el-radio-group v-model="newPlanForm.status">
<el-radio-group v-model="planForm.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">禁用</el-radio>
</el-radio-group>
@ -275,74 +284,15 @@
</el-form>
<template #footer>
<el-button @click="newPlanDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleNewPlanSubmit">确定</el-button>
<el-button @click="planDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handlePlanSubmit">确定</el-button>
</template>
</el-dialog>
<!-- 计划详情弹窗 -->
<el-dialog
v-model="detailDialogVisible"
title="巡护计划详情"
width="600px"
>
<el-descriptions v-if="currentPlan" :column="1" border>
<el-descriptions-item label="计划名称">
{{ currentPlan.plan_name }}
</el-descriptions-item>
<el-descriptions-item label="计划类型">
<el-tag :type="currentPlan.plan_type === 'daily' ? 'info' : 'warning'">
{{ currentPlan.plan_type === 'daily' ? '日常巡护' : '专项巡护' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="执行时间">
{{ formatDateTime(currentPlan.start_date, 'YYYY-MM-DD') }}
{{ formatDateTime(currentPlan.end_date, 'YYYY-MM-DD') }}
</el-descriptions-item>
<el-descriptions-item label="任务频次">
{{ {
'daily': '每日',
'weekly': '每周',
'monthly': '每月'
}[currentPlan.task_frequency] || '-' }}
</el-descriptions-item>
<el-descriptions-item label="巡护区域">
<div v-if="currentPlan.area_scope?.length">
<div v-for="(point, index) in currentPlan.area_scope" :key="index">
经度: {{ point.longitude }}, 纬度: {{ point.latitude }}
</div>
</div>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="计划状态">
<el-tag :type="currentPlan.status === 1 ? 'success' : 'info'">
{{ currentPlan.status === 1 ? '启用' : '禁用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="计划描述">
{{ currentPlan.description || '-' }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDateTime(currentPlan.created_at) }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ formatDateTime(currentPlan.updated_at) }}
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Plus, Delete, Refresh } from '@element-plus/icons-vue'
import { formatDateTime } from '@/utils/format'
@ -375,7 +325,10 @@ const dateRange = ref(['', ''])
//
const newPlanDialogVisible = ref(false)
const newPlanForm = reactive({
const planDialogVisible = ref(false)
const isEdit = ref(false)
const planForm = reactive({
id: '',
plan_name: '',
plan_type: 'daily',
description: '',
@ -386,7 +339,7 @@ const newPlanForm = reactive({
status: 1
})
const newPlanFormRef = ref()
const planFormRef = ref()
//
const rules = {
@ -400,7 +353,7 @@ const rules = {
return
}
try {
const res = await checkPlanName(value, newPlanForm.id)
const res = await checkPlanName(value, planForm.id)
if (res.success && !res.data.available) {
callback(new Error('计划名称已存在'))
} else {
@ -420,7 +373,7 @@ const rules = {
{ required: true, message: '请选择结束日期', trigger: 'change' },
{
validator: (rule, value, callback) => {
if (value && newPlanForm.start_date && value <= newPlanForm.start_date) {
if (value && planForm.start_date && value <= planForm.start_date) {
callback(new Error('结束日期必须大于开始日期'))
} else {
callback()
@ -473,7 +426,7 @@ const getList = async () => {
//
const resetForm = () => {
Object.assign(newPlanForm, {
Object.assign(planForm, {
plan_name: '',
plan_type: 'daily',
description: '',
@ -484,45 +437,71 @@ const resetForm = () => {
status: 1
})
addressList.value = [] //
if (newPlanFormRef.value) {
newPlanFormRef.value.resetFields()
if (planFormRef.value) {
planFormRef.value.resetFields()
}
}
//
const handleNewPlanSubmit = async () => {
if (!newPlanFormRef.value) return
const handlePlanSubmit = async () => {
if (!planFormRef.value) return
try {
await newPlanFormRef.value.validate()
await planFormRef.value.validate()
// area_scope
const formattedAreaScope = addressList.value.map(point => ({
longitude: point.longitude,
latitude: point.latitude,
address: point.address
}))
const submitData = {
plan_name: newPlanForm.plan_name.trim(),
plan_type: newPlanForm.plan_type,
description: newPlanForm.description?.trim(),
start_date: newPlanForm.start_date,
end_date: newPlanForm.end_date,
area_scope: newPlanForm.area_scope,
task_frequency: newPlanForm.task_frequency,
status: newPlanForm.status
plan_name: planForm.plan_name.trim(),
plan_type: planForm.plan_type,
description: planForm.description?.trim(),
start_date: planForm.start_date,
end_date: planForm.end_date,
area_scope: formattedAreaScope,
task_frequency: planForm.task_frequency,
status: planForm.status
}
const res = await createPlan(submitData)
if (res.success) {
ElMessage.success('创建成功')
newPlanDialogVisible.value = false
getList()
if (isEdit.value) {
const res = await updatePlan(planForm.id, submitData)
if (res.success) {
ElMessage.success('更新成功')
planDialogVisible.value = false
getList()
} else {
ElMessage.error(res.message || '更新失败')
}
} else {
ElMessage.error(res.message || '创建失败')
const res = await createPlan(submitData)
if (res.success) {
ElMessage.success('创建成功')
planDialogVisible.value = false
getList()
} else {
ElMessage.error(res.message || '创建失败')
}
}
} catch (error) {
console.error('创建计划错误:', error)
ElMessage.error('创建失败,请检查输入是否正确')
console.error('提交计划错误:', error)
ElMessage.error('提交失败,请检查输入是否正确')
}
}
//
//
const handleCreate = () => {
isEdit.value = false
resetForm()
planDialogVisible.value = true
}
//
const handleDialogClose = () => {
isEdit.value = false
resetForm()
}
@ -554,26 +533,6 @@ const handleSizeChange = (size) => {
getList()
}
//
const detailDialogVisible = ref(false)
const currentPlan = ref(null)
//
const handleView = async (row) => {
try {
const res = await getPlanDetail(row.id)
if (res.success) {
currentPlan.value = res.data
detailDialogVisible.value = true
} else {
ElMessage.error(res.message || '获取计划详情失败')
}
} catch (error) {
console.error('获取计划详情错误:', error)
ElMessage.error('获取计划详情失败')
}
}
//
const handleStatusChange = async (row) => {
try {
@ -591,9 +550,33 @@ const handleStatusChange = async (row) => {
}
}
//
//
const selectedRows = ref([])
//
const canDeletePlan = (plan) => {
//
if (plan.status === 1) return false
//
if (plan.has_related_tasks) return false
//
return true
}
//
const hasSelectedDeletableRows = computed(() => {
return selectedRows.value.some(row => canDeletePlan(row))
})
//
const handleDelete = (row) => {
ElMessageBox.confirm('确定要删除该计划吗?', '提示', {
//
if (!canDeletePlan(row)) {
ElMessage.warning('该巡护计划已有关联的巡护任务或正在进行中,不能删除')
return
}
ElMessageBox.confirm('确认删除该巡护计划吗?此操作不可恢复', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
@ -607,41 +590,62 @@ const handleDelete = (row) => {
ElMessage.error(res.message || '删除失败')
}
} catch (error) {
console.error('删除计划错误:', error)
ElMessage.error('删除失败')
console.error('删除巡护计划错误:', error)
//
if (error.response?.status === 400) {
ElMessage.warning('该巡护计划已有关联的巡护任务或正在进行中,不能删除')
} else {
ElMessage.error('删除失败,请稍后重试')
}
}
}).catch(() => {})
}
//
//
const handleBatchDelete = () => {
if (selectedPlans.value.length === 0) {
ElMessage.warning('请选择要删除的计划')
//
const deletableRows = selectedRows.value.filter(row => canDeletePlan(row))
if (deletableRows.length === 0) {
ElMessage.warning('选中的计划中没有可删除的计划(不能删除进行中或已有关联任务的计划)')
return
}
ElMessageBox.confirm(`确定要删除选中的 ${selectedPlans.value.length} 个计划吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
ElMessageBox.confirm(
`确认删除选中的 ${deletableRows.length} 个计划吗?此操作不可恢复`,
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
try {
const ids = selectedPlans.value.map(plan => plan.id)
const res = await batchDeletePlans(ids)
const res = await batchDeletePlans(deletableRows.map(row => row.id))
if (res.success) {
ElMessage.success('批量删除成功')
selectedPlans.value = []
getList()
//
selectedRows.value = []
} else {
ElMessage.error(res.message || '批量删除失败')
}
} catch (error) {
console.error('批量删除计划错误:', error)
ElMessage.error('批量删除失败')
console.error('批量删除巡护计划错误:', error)
if (error.response?.status === 400) {
ElMessage.warning('部分计划已有关联的巡护任务或正在进行中,不能删除')
} else {
ElMessage.error('批量删除失败,请稍后重试')
}
}
}).catch(() => {})
}
//
const handleSelectionChange = (selection) => {
selectedRows.value = selection
}
//
const handleRefresh = () => {
getList()
@ -685,7 +689,7 @@ const handleAddAddress = async () => {
const result = await convertAddressToCoords(addressInput.value.trim())
if (result) {
addressList.value.push(result)
newPlanForm.area_scope = addressList.value.map(item => ({
planForm.area_scope = addressList.value.map(item => ({
longitude: item.longitude,
latitude: item.latitude
}))
@ -696,18 +700,60 @@ const handleAddAddress = async () => {
//
const handleRemoveAddress = (index) => {
addressList.value.splice(index, 1)
newPlanForm.area_scope = addressList.value.map(item => ({
planForm.area_scope = addressList.value.map(item => ({
longitude: item.longitude,
latitude: item.latitude
}))
}
//
const handleEdit = async (row) => {
try {
const res = await getPlanDetail(row.id)
if (res.success) {
//
Object.assign(planForm, {
id: row.id,
plan_name: res.data.plan_name,
plan_type: res.data.plan_type,
description: res.data.description || '',
start_date: res.data.start_date,
end_date: res.data.end_date,
task_frequency: res.data.task_frequency || 'daily',
status: res.data.status,
area_scope: [] // area_scope
})
// area_scope
const formattedAreaScope = (res.data.area_scope || []).map(point => ({
address: point.address || '未知地址',
longitude: point.longitude,
latitude: point.latitude
}))
addressList.value = formattedAreaScope
planForm.area_scope = formattedAreaScope
isEdit.value = true
planDialogVisible.value = true
} else {
ElMessage.error(res.message || '获取计划详情失败')
}
} catch (error) {
console.error('获取计划详情错误:', error)
ElMessage.error('获取计划详情失败')
}
}
onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
@use "../../../styles/variables.scss" as *;
@use "sass:color";
.patrol-plans {
padding: 16px;
background-color: #f0f2f5;
@ -901,21 +947,283 @@ onMounted(() => {
.address-input-container {
.address-input {
margin-bottom: 12px;
margin-bottom: 16px;
.address-input-field {
width: 100%;
}
.el-input-group__append {
background-color: $primary-color;
border-color: $primary-color;
color: white;
padding: 0 16px;
border-radius: 0 8px 8px 0;
&:hover {
background-color: color.adjust($primary-color, $lightness: 10%);
}
}
}
.address-list {
margin-bottom: 12px;
max-height: 240px;
margin: 16px 0;
border-radius: 8px;
overflow: hidden;
max-height: 200px;
overflow-y: auto;
:deep(.el-table) {
border: 1px solid #ebeef5;
.el-table__inner-wrapper {
max-height: 200px;
}
th {
background-color: #f5f7fa;
color: $text-primary;
font-weight: 500;
padding: 8px 0;
height: 40px;
}
td {
padding: 8px 0;
height: 40px;
}
.el-table__body-wrapper {
overflow-y: auto;
max-height: 160px;
}
}
}
.address-tip {
margin-top: 8px;
margin-top: 12px;
:deep(.el-alert) {
border-radius: 8px;
.el-alert__title {
font-size: 13px;
line-height: 1.5;
}
}
}
}
.plan-form {
.el-form-item {
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
:deep(.el-form-item__label) {
font-weight: 500;
color: $text-primary;
padding-right: 16px;
}
:deep(.el-input__wrapper),
:deep(.el-textarea__inner) {
box-shadow: none;
border: 1px solid #dcdfe6;
border-radius: 8px;
transition: all 0.3s;
&:hover {
border-color: #c0c4cc;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
}
&.is-focus {
border-color: $primary-color;
box-shadow: 0 0 0 1px $primary-color inset;
}
}
:deep(.el-input__inner) {
height: 40px;
line-height: 40px;
padding: 0 16px;
}
:deep(.el-textarea__inner) {
padding: 12px 16px;
min-height: 120px;
resize: vertical;
}
:deep(.el-select) {
width: 100%;
.el-input__wrapper {
border-radius: 8px;
}
}
:deep(.el-date-editor) {
width: 100%;
.el-input__wrapper {
border-radius: 8px;
}
}
:deep(.el-radio-group) {
display: flex;
gap: 24px;
padding: 8px 0;
.el-radio {
margin-right: 0;
.el-radio__label {
font-size: 14px;
color: $text-regular;
}
.el-radio__input.is-checked + .el-radio__label {
color: $primary-color;
}
}
}
}
.address-input-container {
.address-input {
margin-bottom: 16px;
.address-input-field {
width: 100%;
}
.el-input-group__append {
background-color: $primary-color;
border-color: $primary-color;
color: white;
padding: 0 16px;
border-radius: 0 8px 8px 0;
&:hover {
background-color: color.adjust($primary-color, $lightness: 10%);
}
}
}
.address-list {
margin: 16px 0;
border-radius: 8px;
overflow: hidden;
max-height: 200px;
overflow-y: auto;
:deep(.el-table) {
border: 1px solid #ebeef5;
.el-table__inner-wrapper {
max-height: 200px;
}
th {
background-color: #f5f7fa;
color: $text-primary;
font-weight: 500;
padding: 8px 0;
height: 40px;
}
td {
padding: 8px 0;
height: 40px;
}
.el-table__body-wrapper {
overflow-y: auto;
max-height: 160px;
}
}
}
.address-tip {
margin-top: 12px;
:deep(.el-alert) {
border-radius: 8px;
.el-alert__title {
font-size: 13px;
line-height: 1.5;
}
}
}
}
}
:deep(.el-dialog) {
border-radius: 16px;
overflow: hidden;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1);
.el-dialog__header {
margin: 0;
padding: 20px 24px;
border-bottom: 1px solid #ebeef5;
background-color: #fafafa;
.el-dialog__title {
font-size: 18px;
font-weight: 600;
color: $text-primary;
}
.el-dialog__headerbtn {
top: 20px;
right: 24px;
}
}
.el-dialog__body {
padding: 24px;
}
.el-dialog__footer {
padding: 16px 24px;
margin: 0;
border-top: 1px solid #ebeef5;
background-color: #fafafa;
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
.el-button {
min-width: 100px;
padding: 12px 24px;
border-radius: 8px;
font-weight: 500;
transition: all 0.3s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
&.el-button--primary {
background: linear-gradient(45deg, $primary-color, color.adjust($primary-color, $lightness: 10%));
border: none;
&:hover {
opacity: 0.9;
}
}
}
}
}
}
</style>

View File

@ -1,7 +1,7 @@
<script setup>
import { ref, reactive, computed, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Search, Plus, InfoFilled } from '@element-plus/icons-vue';
import { Search, Plus, InfoFilled, Delete, Refresh } from '@element-plus/icons-vue';
import { formatDateTime } from '@/utils/format';
import { getRoutePointsWithAddress } from '@/utils/map';
import {
@ -9,9 +9,12 @@ import {
createTask,
getTaskDetail,
updateTask,
cancelTask
cancelTask,
deleteTask,
batchDeleteTasks
} from '@/api/patrol/task';
import { getUserList } from '@/api/user';
import { getPlanList, getPlanDetail } from '@/api/patrol/plan';
//
const tableData = ref([]);
@ -20,6 +23,12 @@ const loading = ref(false);
//
const userList = ref([]);
//
const selectedRows = ref([]);
//
const planList = ref([]);
//
const getList = async () => {
loading.value = true;
@ -69,6 +78,22 @@ const getUserData = async () => {
}
};
//
const getPlanData = async () => {
try {
const res = await getPlanList({ status: 1 }); //
if (res.success && Array.isArray(res.data.list)) {
planList.value = res.data.list;
} else {
console.error('获取计划列表数据格式错误:', res);
planList.value = [];
}
} catch (error) {
console.error('获取计划列表错误:', error);
planList.value = [];
}
};
//
const getPriorityText = (priority) => {
const map = {
@ -117,7 +142,23 @@ const handleNewTaskSubmit = async () => {
try {
await newTaskFormRef.value.validate();
//
//
const planRes = await getPlanDetail(newTaskForm.plan_id);
if (!planRes.success) {
ElMessage.error('获取计划详情失败');
return;
}
const planStartDate = new Date(planRes.data.start_date);
const planEndDate = new Date(planRes.data.end_date);
const patrolDate = new Date(newTaskForm.patrol_date);
if (patrolDate < planStartDate || patrolDate > planEndDate) {
ElMessage.error('巡护日期必须在计划时间范围内');
return;
}
// API
const submitData = {
plan_id: Number(newTaskForm.plan_id),
task_name: newTaskForm.task_name.trim(),
@ -125,11 +166,11 @@ const handleNewTaskSubmit = async () => {
patrol_date: newTaskForm.patrol_date,
start_time: newTaskForm.start_time,
end_time: newTaskForm.end_time,
route_points: newTaskForm.route_points,
route_points: newTaskForm.route_points || [],
executor_ids: newTaskForm.executor_ids,
description: newTaskForm.description?.trim(),
priority: newTaskForm.priority,
status: newTaskForm.status
description: newTaskForm.description?.trim() || '',
priority: Number(newTaskForm.priority),
status: 0
};
const res = await createTask(submitData);
@ -225,6 +266,7 @@ const resetFilters = () => {
onMounted(() => {
getUserData(); //
getPlanData(); //
getList();
});
@ -444,6 +486,233 @@ const getUserNames = (ids) => {
return `${ids.length}`;
}
};
//
const canDeleteTask = (task) => {
//
if (task.status === 1) return false
//
if (task.has_patrol_records) return false
//
return true
}
//
const hasSelectedDeletableRows = computed(() => {
return selectedRows.value.some(row => canDeleteTask(row))
})
//
const handleDelete = (row) => {
//
if (!canDeleteTask(row)) {
ElMessage.warning('该巡护任务正在进行中或已有巡护记录,不能删除')
return
}
ElMessageBox.confirm('确认删除该巡护任务吗?此操作不可恢复', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteTask(row.id)
if (res.success) {
ElMessage.success('删除成功')
getList()
} else {
ElMessage.error(res.message || '删除失败')
}
} catch (error) {
console.error('删除巡护任务错误:', error)
//
if (error.response?.status === 400) {
ElMessage.warning('该巡护任务正在进行中或已有巡护记录,不能删除')
} else {
ElMessage.error('删除失败,请稍后重试')
}
}
}).catch(() => {})
}
//
const handleBatchDelete = () => {
//
const deletableRows = selectedRows.value.filter(row => canDeleteTask(row))
if (deletableRows.length === 0) {
ElMessage.warning('选中的任务中没有可删除的任务(不能删除进行中或已有巡护记录的任务)')
return
}
ElMessageBox.confirm(
`确认删除选中的 ${deletableRows.length} 个任务吗?此操作不可恢复`,
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
try {
const res = await batchDeleteTasks(deletableRows.map(row => row.id))
if (res.success) {
ElMessage.success('批量删除成功')
getList()
//
selectedRows.value = []
} else {
ElMessage.error(res.message || '批量删除失败')
}
} catch (error) {
console.error('批量删除巡护任务错误:', error)
if (error.response?.status === 400) {
ElMessage.warning('部分任务正在进行中或已有巡护记录,不能删除')
} else {
ElMessage.error('批量删除失败,请稍后重试')
}
}
}).catch(() => {})
}
//
const handleSelectionChange = (selection) => {
selectedRows.value = selection
}
//
const dialogVisible = ref(false)
const dialogTitle = ref('新增任务')
const formMode = ref('create')
const form = ref({
task_name: '',
description: '',
patrol_date: '',
start_time: '',
end_time: '',
executor_ids: [],
route_points: [],
task_type: 'regular',
priority: 2,
plan_id: ''
})
//
const formRules = {
task_name: [
{ required: true, message: '请输入任务名称', trigger: 'blur' },
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入任务描述', trigger: 'blur' }
],
patrol_date: [
{ required: true, message: '请选择巡护日期', trigger: 'change' }
],
start_time: [
{ required: true, message: '请选择开始时间', trigger: 'change' }
],
end_time: [
{ required: true, message: '请选择结束时间', trigger: 'change' }
],
executor_ids: [
{ required: true, message: '请选择执行人', trigger: 'change' }
],
plan_id: [
{ required: true, message: '请输入计划ID', trigger: 'blur' }
]
}
const formRef = ref(null)
//
const handleEdit = async (row) => {
try {
const res = await getTaskDetail(row.id)
if (res.success) {
formMode.value = 'edit'
dialogTitle.value = '编辑任务'
form.value = {
id: row.id,
task_name: res.data.task_name,
description: res.data.description,
patrol_date: res.data.patrol_date,
start_time: res.data.start_time,
end_time: res.data.end_time,
executor_ids: res.data.executor_ids || [],
route_points: res.data.route_points || [],
task_type: res.data.task_type || 'regular',
priority: res.data.priority || 2,
plan_id: res.data.plan_id
}
dialogVisible.value = true
} else {
ElMessage.error(res.message || '获取任务详情失败')
}
} catch (error) {
console.error('获取任务详情错误:', error)
ElMessage.error('获取任务详情失败')
}
}
//
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
try {
const submitData = {
task_name: form.value.task_name.trim(),
description: form.value.description.trim(),
patrol_date: form.value.patrol_date,
start_time: form.value.start_time,
end_time: form.value.end_time,
executor_ids: form.value.executor_ids,
route_points: form.value.route_points,
task_type: form.value.task_type,
priority: Number(form.value.priority),
plan_id: Number(form.value.plan_id)
}
let res
if (formMode.value === 'create') {
res = await createTask(submitData)
} else {
res = await updateTask(form.value.id, submitData)
}
if (res.success) {
ElMessage.success(`${formMode.value === 'create' ? '新增' : '编辑'}成功`)
dialogVisible.value = false
getList()
} else {
ElMessage.error(res.message || `${formMode.value === 'create' ? '新增' : '编辑'}失败`)
}
} catch (error) {
console.error(`${formMode.value === 'create' ? '新增' : '编辑'}任务错误:`, error)
ElMessage.error('操作失败,请检查输入是否正确')
}
}
})
}
//
const handlePlanChange = async (planId) => {
if (!planId) return;
try {
const res = await getPlanDetail(planId);
if (res.success) {
//
newTaskForm.patrol_date = '';
//
}
} catch (error) {
console.error('获取计划详情错误:', error);
ElMessage.error('获取计划详情失败');
}
};
</script>
<template>
@ -554,16 +823,23 @@ const getUserNames = (ids) => {
</div>
<div class="right">
<el-button @click="resetFilters">重置</el-button>
<el-button type="primary" @click="handleNewTask">新建任务</el-button>
</div>
</div>
<!-- 操作按钮区域 -->
<div class="operation-btns">
<el-button type="primary" :icon="Plus" @click="handleNewTask">新建任务</el-button>
<el-button :icon="Delete" @click="handleBatchDelete" :disabled="!hasSelectedDeletableRows">批量删除</el-button>
<el-button :icon="Refresh" @click="handleSearch">刷新</el-button>
</div>
<!-- 巡护任务列表 -->
<el-table
v-loading="loading"
:data="tableData"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="plan_id" label="计划ID" width="90" align="center">
<template #default="{ row }">
@ -620,21 +896,19 @@ const getUserNames = (ids) => {
{{ formatDateTime(row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right" align="center">
<el-table-column label="操作" width="200" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link @click="handleView(row)">查看</el-button>
<el-button
v-if="row.status === 0"
type="primary"
link
@click="handleEdit(row)"
>编辑</el-button>
<el-button
v-if="row.status === 0"
type="danger"
link
@click="handleCancel(row)"
>取消</el-button>
@click="handleDelete(row)"
v-if="canDeleteTask(row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
@ -771,15 +1045,32 @@ const getUserNames = (ids) => {
>
<el-form ref="newTaskFormRef" :model="newTaskForm" :rules="rules" label-width="100px">
<el-form-item label="关联计划" prop="plan_id">
<el-input v-model="newTaskForm.plan_id" placeholder="请输入计划ID" type="number" />
<el-select
v-model="newTaskForm.plan_id"
placeholder="请选择关联计划"
style="width: 100%"
@change="handlePlanChange"
>
<el-option
v-for="plan in planList"
:key="plan.id"
:label="plan.plan_name"
:value="plan.id"
>
<span>{{ plan.plan_name }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">
{{ plan.plan_type === 'daily' ? '日常巡护' : '专项巡护' }}
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="任务名称" prop="task_name">
<el-input v-model="newTaskForm.task_name" placeholder="请输入任务名称" maxlength="100" show-word-limit />
</el-form-item>
<el-form-item label="任务类型" prop="task_type">
<el-radio-group v-model="newTaskForm.task_type">
<el-radio :value="'regular'" label="常规">常规</el-radio>
<el-radio :value="'emergency'" label="紧急">紧急</el-radio>
<el-radio value="regular">常规</el-radio>
<el-radio value="emergency">紧急</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="巡护日期" prop="patrol_date">
@ -871,6 +1162,108 @@ const getUserNames = (ids) => {
@current-change="handlePageChange"
/>
</div>
<!-- 添加编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="600px"
destroy-on-close
>
<el-form
ref="formRef"
:model="form"
:rules="formRules"
label-width="100px"
>
<el-form-item label="计划ID" prop="plan_id">
<el-input v-model="form.plan_id" placeholder="请输入计划ID" type="number" />
</el-form-item>
<el-form-item label="任务名称" prop="task_name">
<el-input v-model="form.task_name" placeholder="请输入任务名称" />
</el-form-item>
<el-form-item label="任务类型" prop="task_type">
<el-radio-group v-model="form.task_type">
<el-radio value="regular">常规</el-radio>
<el-radio value="emergency">紧急</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="巡护日期" prop="patrol_date">
<el-date-picker
v-model="form.patrol_date"
type="date"
placeholder="选择巡护日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="执行时间" required>
<el-col :span="11">
<el-form-item prop="start_time">
<el-time-picker
v-model="form.start_time"
placeholder="开始时间"
format="HH:mm"
value-format="HH:mm"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="2" class="text-center">
<span class="text-gray-500">-</span>
</el-col>
<el-col :span="11">
<el-form-item prop="end_time">
<el-time-picker
v-model="form.end_time"
placeholder="结束时间"
format="HH:mm"
value-format="HH:mm"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-form-item>
<el-form-item label="执行人" prop="executor_ids">
<el-select
v-model="form.executor_ids"
multiple
placeholder="请选择执行人"
style="width: 100%"
>
<el-option
v-for="user in userList"
:key="user.id"
:label="user.real_name || user.username"
:value="user.id"
/>
</el-select>
</el-form-item>
<el-form-item label="优先级" prop="priority">
<el-radio-group v-model="form.priority">
<el-radio :value="1"></el-radio>
<el-radio :value="2"></el-radio>
<el-radio :value="3"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="任务描述" prop="description">
<el-input
v-model="form.description"
type="textarea"
:rows="4"
placeholder="请输入任务描述"
maxlength="1000"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
@ -1068,6 +1461,15 @@ const getUserNames = (ids) => {
}
}
.operation-btns {
padding: 16px 0;
display: flex;
justify-content: flex-end;
gap: 12px;
border-bottom: 1px solid $border-color;
margin-bottom: 16px;
}
.mt-20 {
margin-top: 20px;
}

View File

@ -394,8 +394,8 @@ onMounted(() => {
</el-form-item>
<el-form-item label="图片" prop="image_url">
<el-radio-group v-model="form.imageInputType" class="mb-4" @change="handleImageTypeChange">
<el-radio :value="'url'">输入图片地址</el-radio>
<el-radio :value="'upload'">上传图片</el-radio>
<el-radio value="url">输入图片地址</el-radio>
<el-radio value="upload">上传图片</el-radio>
</el-radio-group>
<template v-if="form.imageInputType === 'url'">

View File

@ -193,8 +193,8 @@ const icons = {
</el-form-item>
<el-form-item label="备份类型">
<el-radio-group v-model="backupConfig.backupType">
<el-radio label="full">完整备份</el-radio>
<el-radio label="data">数据备份</el-radio>
<el-radio value="full">完整备份</el-radio>
<el-radio value="data">数据备份</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="保留时间">