Files
water-management-system/frontend/src/views/patrol/WorkOrderManagementView.vue
T
bot_dev1 6dcb535874 feat: [Issue#77] 实现巡检问题上报 + 工单联动功能
- 新增巡检问题上报表 (patrol_problem)
- 新增工单表 (work_order)
- 新增工单处理记录表 (work_order_process)
- 新增巡检问题与工单关联触发表 (patrol_work_order_trigger)
- 实现PatrolProblemService和WorkOrderService业务逻辑
- 实现RESTful API接口
- 开发前端问题上报和工单管理界面
- 添加JSON列表TypeHandler处理图片URL数组
- 更新数据库schema支持新功能

完成了issue要求的:
✅ 巡检中问题上报(类型/描述/拍照)
✅ 自动创建工单
✅ 处理跟踪
2026-06-14 16:05:51 +08:00

552 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="work-order-management">
<el-card class="work-order-stats">
<template #header>
<span>工单统计</span>
</template>
<el-row :gutter="20">
<el-col :span="6">
<div class="stat-item">
<div class="stat-number">{{ statistics.totalOrders }}</div>
<div class="stat-label">总工单数</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-item">
<div class="stat-number">{{ statistics.pendingCount }}</div>
<div class="stat-label">待处理</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-item">
<div class="stat-number">{{ statistics.processingCount }}</div>
<div class="stat-label">处理中</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-item">
<div class="stat-number">{{ statistics.completedCount }}</div>
<div class="stat-label">已完成</div>
</div>
</el-col>
</el-row>
</el-card>
<el-card class="work-order-list">
<template #header>
<div class="card-header">
<span>工单列表</span>
<div class="header-actions">
<el-select v-model="statusFilter" placeholder="状态筛选" clearable style="width: 120px; margin-right: 10px;">
<el-option label="待处理" value="pending" />
<el-option label="已分配" value="assigned" />
<el-option label="处理中" value="processing" />
<el-option label="已完成" value="completed" />
<el-option label="已取消" value="cancelled" />
</el-select>
<el-input
v-model="searchQuery"
placeholder="搜索工单..."
style="width: 200px"
clearable
/>
<el-button type="primary" @click="createNewWorkOrder">新建工单</el-button>
</div>
</div>
</template>
<el-table
:data="filteredWorkOrders"
stripe
style="width: 100%"
v-loading="loading"
>
<el-table-column prop="orderNo" label="工单编号" width="120" />
<el-table-column prop="title" label="工单标题" min-width="200" />
<el-table-column prop="orderType" label="工单类型" width="120" />
<el-table-column prop="priority" label="优先级" width="80">
<template #default="{ row }">
<el-tag :type="getPriorityType(row.priority)">
{{ getPriorityText(row.priority) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="assigneeName" label="处理人" width="100" />
<el-table-column prop="location" label="位置" width="150" />
<el-table-column prop="createdAt" label="创建时间" width="180">
<template #default="{ row }">
{{ formatDate(row.createdAt) }}
</template>
</el-table-column>
<el-table-column prop="completionTime" label="完成时间" width="180">
<template #default="{ row }">
{{ row.completionTime ? formatDate(row.completionTime) : '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<el-button
size="small"
@click="viewWorkOrder(row)"
>
查看
</el-button>
<el-button
size="small"
type="primary"
@click="assignWorkOrder(row)"
v-if="row.status === 'pending'"
>
分派
</el-button>
<el-button
size="small"
type="success"
@click="startWorkOrder(row)"
v-if="row.status === 'assigned'"
>
开始处理
</el-button>
<el-button
size="small"
type="info"
@click="completeWorkOrder(row)"
v-if="row.status === 'processing'"
>
完成
</el-button>
<el-button
size="small"
type="danger"
@click="cancelWorkOrder(row)"
v-if="row.status !== 'completed' && row.status !== 'cancelled'"
>
取消
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="totalWorkOrders"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 工单详情对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="80%"
:before-close="handleDialogClose"
>
<div v-if="currentWorkOrder">
<el-descriptions :column="2" border>
<el-descriptions-item label="工单编号">{{ currentWorkOrder.orderNo }}</el-descriptions-item>
<el-descriptions-item label="工单类型">{{ currentWorkOrder.orderType }}</el-descriptions-item>
<el-descriptions-item label="优先级">
<el-tag :type="getPriorityType(currentWorkOrder.priority)">
{{ getPriorityText(currentWorkOrder.priority) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusType(currentWorkOrder.status)">
{{ getStatusText(currentWorkOrder.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="处理人">{{ currentWorkOrder.assigneeName }}</el-descriptions-item>
<el-descriptions-item label="预计工时">{{ currentWorkOrder.estimatedDuration }}分钟</el-descriptions-item>
<el-descriptions-item label="问题标题">{{ currentWorkOrder.title }}</el-descriptions-item>
<el-descriptions-item label="位置">{{ currentWorkOrder.location }}</el-descriptions-item>
<el-descriptions-item label="问题描述" :span="2">{{ currentWorkOrder.description }}</el-descriptions-item>
<el-descriptions-item label="解决方案" :span="2">{{ currentWorkOrder.solutionDescription || '-' }}</el-descriptions-item>
<el-descriptions-item label="处理结果" :span="2">{{ currentWorkOrder.solutionResult || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户反馈" :span="2">{{ currentWorkOrder.customerFeedback || '-' }}</el-descriptions-item>
</el-descriptions>
<!-- 处理记录 -->
<div style="margin-top: 20px;">
<h4>处理记录</h4>
<el-timeline>
<el-timeline-item
v-for="record in processRecords"
:key="record.id"
:timestamp="formatDate(record.createdAt)"
:type="getProcessStepType(record.processStep)"
>
<h5>{{ getProcessStepText(record.processStep) }}</h5>
<p>{{ record.comment }}</p>
<p v-if="record.processorName">处理人:{{ record.processorName }}</p>
<div v-if="record.photos && record.photos.length > 0">
<el-image
v-for="(photo, index) in record.photos"
:key="index"
:src="photo"
style="width: 100px; height: 100px; margin-right: 10px; margin-top: 10px;"
fit="cover"
:preview-src-list="record.photos"
/>
</div>
</el-timeline-item>
</el-timeline>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import axios from 'axios'
const workOrders = ref([])
const statistics = ref({
totalOrders: 0,
pendingCount: 0,
assignedCount: 0,
processingCount: 0,
completedCount: 0,
cancelledCount: 0
})
const loading = ref(false)
const dialogVisible = ref(false)
const currentWorkOrder = ref(null)
const processRecords = ref([])
const statusFilter = ref('')
const searchQuery = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const totalWorkOrders = ref(0)
// 计算属性
const filteredWorkOrders = computed(() => {
let filtered = workOrders.value
// 状态筛选
if (statusFilter.value) {
filtered = filtered.filter(w => w.status === statusFilter.value)
}
// 搜索筛选
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
filtered = filtered.filter(w =>
w.title.toLowerCase().includes(query) ||
w.orderNo.toLowerCase().includes(query) ||
w.location.toLowerCase().includes(query)
)
}
return filtered
})
const dialogTitle = computed(() => {
return currentWorkOrder.value ? `工单详情 - ${currentWorkOrder.value.orderNo}` : '工单详情'
})
// 获取工单列表
const fetchWorkOrders = async () => {
loading.value = true
try {
const response = await axios.get('/api/work-orders/status/pending')
workOrders.value = response.data
totalWorkOrders.value = workOrders.value.length
await fetchStatistics()
} catch (error) {
console.error('获取工单列表失败:', error)
ElMessage.error('获取工单列表失败')
} finally {
loading.value = false
}
}
// 获取统计信息
const fetchStatistics = async () => {
try {
const response = await axios.get('/api/work-orders/statistics')
statistics.value = response.data
} catch (error) {
console.error('获取统计信息失败:', error)
}
}
// 创建新工单
const createNewWorkOrder = () => {
ElMessage.info('跳转到新建工单页面')
}
// 查看工单详情
const viewWorkOrder = async (workOrder) => {
currentWorkOrder.value = workOrder
dialogVisible.value = true
// 获取处理记录
try {
const response = await axios.get(`/api/work-orders/process/${workOrder.id}`)
processRecords.value = response.data
} catch (error) {
console.error('获取处理记录失败:', error)
}
}
// 分派工单
const assignWorkOrder = (workOrder) => {
ElMessageBox.prompt(
'请输入处理人ID',
'分派工单',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^\d+$/,
inputErrorMessage: '请输入有效的用户ID'
}
).then(async ({ value }) => {
try {
const assigneeName = '处理人' // 实际应用中应该根据ID获取用户名
const response = await axios.put(`/api/work-orders/${workOrder.id}/assign`, {
assigneeId: parseInt(value),
assigneeName: assigneeName
})
if (response.data) {
ElMessage.success('工单分派成功')
fetchWorkOrders()
}
} catch (error) {
console.error('分派工单失败:', error)
ElMessage.error('分派工单失败')
}
}).catch(() => {
// 用户取消
})
}
// 开始处理工单
const startWorkOrder = async (workOrder) => {
try {
await ElMessageBox.confirm(
`确认为工单 "${workOrder.title}" 开始处理吗?`,
'开始处理',
{ confirmButtonText: '确定', cancelButtonText: '取消' }
)
const response = await axios.put(`/api/work-orders/${workOrder.id}/start`)
if (response.data) {
ElMessage.success('开始处理成功')
fetchWorkOrders()
}
} catch (error) {
if (error !== 'cancel') {
console.error('开始处理失败:', error)
ElMessage.error('开始处理失败')
}
}
}
// 完成工单
const completeWorkOrder = (workOrder) => {
ElMessageBox.prompt(
'请输入处理结果',
'完成工单',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
inputType: 'textarea',
inputPlaceholder: '请详细描述处理结果'
}
).then(async ({ value }) => {
try {
const response = await axios.put(`/api/work-orders/${workOrder.id}/complete`, {
solutionResult: value
})
if (response.data) {
ElMessage.success('工单完成成功')
fetchWorkOrders()
}
} catch (error) {
console.error('完成工单失败:', error)
ElMessage.error('完成工单失败')
}
}).catch(() => {
// 用户取消
})
}
// 取消工单
const cancelWorkOrder = (workOrder) => {
ElMessageBox.confirm(
`确认为工单 "${workOrder.title}" 取消吗?`,
'取消工单',
{ confirmButtonText: '确定', cancelButtonText: '取消' }
).then(async () => {
try {
const response = await axios.put(`/api/work-orders/${workOrder.id}/status`, {
status: 'cancelled',
processStatus: 'terminated'
})
if (response.data) {
ElMessage.success('工单取消成功')
fetchWorkOrders()
}
} catch (error) {
console.error('取消工单失败:', error)
ElMessage.error('取消工单失败')
}
}).catch(() => {
// 用户取消
})
}
// 处理对话框关闭
const handleDialogClose = () => {
currentWorkOrder.value = null
processRecords.value = []
}
// 辅助函数
const getPriorityType = (priority) => {
switch (priority) {
case 'low': return 'info'
case 'normal': return ''
case 'high': return 'warning'
case 'critical': return 'danger'
default: return ''
}
}
const getPriorityText = (priority) => {
switch (priority) {
case 'low': return '低'
case 'normal': return '普通'
case 'high': return '高'
case 'critical': return '紧急'
default: return priority
}
}
const getStatusType = (status) => {
switch (status) {
case 'pending': return 'warning'
case 'assigned': return 'primary'
case 'processing': = 'primary'
case 'completed': return 'success'
case 'cancelled': return 'info'
default: return ''
}
}
const getStatusText = (status) => {
switch (status) {
case 'pending': return '待处理'
case 'assigned': return '已分配'
case 'processing': return '处理中'
case 'completed': return '已完成'
case 'cancelled': return '已取消'
default: return status
}
}
const getProcessStepType = (step) => {
switch (step) {
case 'created': return 'primary'
case 'accepted': return 'success'
case 'in_progress': return 'warning'
case 'completed': return 'success'
default: return 'primary'
}
}
const getProcessStepText = (step) => {
switch (step) {
case 'created': return '工单创建'
case 'accepted': return '工单接受'
case 'in_progress': return '处理中'
case 'completed': return '工单完成'
default: return step
}
}
const formatDate = (date) => {
if (!date) return ''
return new Date(date).toLocaleString()
}
const handleSizeChange = (val) => {
pageSize.value = val
fetchWorkOrders()
}
const handleCurrentChange = (val) => {
currentPage.value = val
fetchWorkOrders()
}
// 初始化
onMounted(() => {
fetchWorkOrders()
})
</script>
<style scoped>
.work-order-management {
padding: 20px;
}
.work-order-stats {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-actions {
display: flex;
align-items: center;
gap: 10px;
}
.stat-item {
text-align: center;
padding: 20px;
border-radius: 8px;
background: #f5f7fa;
}
.stat-number {
font-size: 24px;
font-weight: bold;
color: #409eff;
margin-bottom: 5px;
}
.stat-label {
color: #606266;
font-size: 14px;
}
.pagination {
margin-top: 20px;
text-align: right;
}
</style>