- 新增巡检问题上报表 (patrol_problem) - 新增工单表 (work_order) - 新增工单处理记录表 (work_order_process) - 新增巡检问题与工单关联触发表 (patrol_work_order_trigger) - 实现PatrolProblemService和WorkOrderService业务逻辑 - 实现RESTful API接口 - 开发前端问题上报和工单管理界面 - 添加JSON列表TypeHandler处理图片URL数组 - 更新数据库schema支持新功能 完成了issue要求的: ✅ 巡检中问题上报(类型/描述/拍照) ✅ 自动创建工单 ✅ 处理跟踪
552 lines
16 KiB
Vue
552 lines
16 KiB
Vue
<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> |