feat: [Issue#77] 实现巡检问题上报 + 工单联动功能
- 新增巡检问题上报表 (patrol_problem) - 新增工单表 (work_order) - 新增工单处理记录表 (work_order_process) - 新增巡检问题与工单关联触发表 (patrol_work_order_trigger) - 实现PatrolProblemService和WorkOrderService业务逻辑 - 实现RESTful API接口 - 开发前端问题上报和工单管理界面 - 添加JSON列表TypeHandler处理图片URL数组 - 更新数据库schema支持新功能 完成了issue要求的: ✅ 巡检中问题上报(类型/描述/拍照) ✅ 自动创建工单 ✅ 处理跟踪
This commit is contained in:
@@ -0,0 +1,552 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user