2025/4/29团队项目开发进度报告
项目收尾工作:
Home.vue:
<!-- vivy -->
<!-- 05/4/29 统一首页 -->
<template>
<div class="home-container">
<el-row :gutter="20">
<!-- 左侧个人信息卡片 -->
<el-col :span="8">
<el-card class="user-card">
<div class="user-info">
<img src="../assets/user.png" alt="" style="width: 120px; height: 120px; border-radius: 50%;">
<h3>{{ userInfo.username || '用户' }}</h3>
<p>工号: {{ userInfo.id || '-' }}</p>
<p>电话: {{ userInfo.phone || '-' }}</p>
</div>
<el-divider></el-divider>
<!-- 工程师工单统计饼图 -->
<div v-if="userRole === 'engineer'" class="engineer-stats">
<h4>我的工单统计</h4>
<div class="pie-chart-container">
<v-chart class="pie-chart" :option="pieChartOption" autoresize />
</div>
</div>
<!-- 管理员工单统计饼图 -->
<div v-if="userRole === 'admin' || userRole === 'super_admin'" class="engineer-stats">
<h4>全部工单统计</h4>
<div class="pie-chart-container">
<v-chart class="pie-chart" :option="pieChartOption" autoresize />
</div>
</div>
</el-card>
</el-col>
<!-- 右侧内容区域 -->
<el-col :span="16">
<!-- Engineer和Super_admin显示待处理任务 -->
<el-card v-if="userRole === 'engineer' || userRole === 'super_admin'" class="task-card">
<template #header>
<div class="card-header">
<span>待接取工单</span>
<el-select v-model="currentOrderType" @change="handleOrderTypeChange" size="small" style="width: 120px">
<el-option label="巡检工单" value="inspection" />
<el-option label="保养工单" value="maintenance" />
<el-option label="维修工单" value="repair" />
<el-option label="检测工单" value="testing" />
</el-select>
</div>
</template>
<div class="task-container">
<el-empty v-if="currentOrders.length === 0" description="暂无待处理工单"></el-empty>
<div v-else class="task-list">
<div v-for="order in currentOrders" :key="order.orderId" class="task-item">
<div class="task-info">
<h4>{{ getOrderTypeLabel(currentOrderType) }} #{{ order.orderId }}</h4>
<p>设备编号: {{ order.deviceId }}</p>
<p>设备类型: {{ order.deviceType }}</p>
<p>设备位置: {{ order.location }}</p>
</div>
<div class="task-action">
<el-tag :type="getStatusType(order.status)">{{ order.status }}</el-tag>
<el-button
size="small"
type="primary"
@click="handleTakeOrder(order)"
style="margin-top: 10px;"
>
接取工单
</el-button>
</div>
</div>
</div>
</div>
</el-card>
<!-- Admin和Super_admin显示故障列表 -->
<el-card v-if="userRole === 'admin' || userRole === 'super_admin'"
class="fault-card"
:style="userRole === 'super_admin' ? 'margin-top: 20px;' : ''">
<template #header>
<div class="card-header">
<span>设备故障列表</span>
<el-button type="primary" size="small" @click="showAddFaultDialog">添加故障报修</el-button>
</div>
</template>
<div class="table-container">
<el-table :data="faultList" border style="width: 100%" height="350">
<el-table-column prop="faultId" label="故障编号" width="90" />
<el-table-column prop="deviceId" label="设备编号" width="90" />
<el-table-column prop="faultDesc" label="故障描述" min-width="120" show-overflow-tooltip />
<el-table-column prop="reportType" label="报修类型" width="90" />
<el-table-column prop="reportedName" label="报修人" width="80" />
<el-table-column prop="reportedPhone" label="联系电话" width="110" />
<el-table-column prop="status" label="状态" width="80">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
<!-- 设备状态统计 -->
<el-card class="device-card" style="margin-top: 20px;">
<template #header>
<div class="card-header">
<span>设备状态概览</span>
</div>
</template>
<!-- 替换原来的状态框为图表 -->
<div class="chart-container">
<v-chart class="chart" :option="chartOption" autoresize />
</div>
</el-card>
</el-col>
</el-row>
<!-- 添加故障报修对话框 -->
<el-dialog
title="设备故障报修"
v-model="dialogVisible"
width="500px"
>
<el-form :model="faultForm" label-width="80px">
<el-form-item label="设备编号">
<el-select v-model="faultForm.deviceId" style="width: 100%">
<el-option
v-for="device in deviceList"
:key="device.deviceId"
:label="device.deviceId + ' - ' + device.deviceType"
:value="device.deviceId"
:disabled="device.status !== '正常'"
/>
</el-select>
</el-form-item>
<el-form-item label="故障描述">
<el-input type="textarea" v-model="faultForm.faultDesc" rows="3" />
</el-form-item>
<el-form-item label="报修类型">
<el-select v-model="faultForm.reportType" style="width: 100%">
<el-option label="自主报修" value="自主报修" />
<el-option label="电话报修" value="电话报修" />
</el-select>
</el-form-item>
<el-form-item label="报修人">
<el-input v-model="faultForm.reportedName" readonly />
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="faultForm.reportedPhone" readonly />
</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>
<script setup>
import { ref, onMounted, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { getDeviceList, updateDeviceStatus, getDeviceStats } from '@/api/device'
import { addDeviceFault } from '@/api/deviceFault'
import { createRepairOrder } from '@/api/repairOrder'
// 引入 ECharts 组件
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { BarChart, PieChart } from 'echarts/charts' // 添加 PieChart
import {TitleComponent, TooltipComponent, GridComponent, LegendComponent} from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'
// 接取任务
import { updateFaultStatus } from '@/api/deviceFault'
// 获取工单相关 API
import * as inspectionOrderApi from '@/api/inspectionOrder'
import * as maintenanceOrderApi from '@/api/maintenanceOrder'
import * as testingOrderApi from '@/api/testingOrder'
import * as repairOrderApi from '@/api/repairOrder'
import { getFaultList } from '@/api/deviceFault'
import { getDevice } from '@/api/device'
// 导入巡检计划API
import { getPlanById } from '@/api/inspection'
// 导入保养计划API
import * as maintenancePlanApi from '@/api/maintenance'
// 导入检测计划API
import * as testingPlanApi from '@/api/testing'
// 注册 ECharts 组件
use([
BarChart,
PieChart, // 添加 PieChart
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
CanvasRenderer
])
// 用户信息
const userInfo = ref({})
const userRole = computed(() => userInfo.value.role || '')
// 工单统计数据
const orderStats = ref({
pending: 0, // 待接取
processing: 0, // 处理中
reviewing: 0, // 待审批
completed: 0 // 已完成
})
// 饼图配置
const pieChartOption = computed(() => {
return {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 0,
left: 'center',
itemWidth: 10,
itemHeight: 10,
textStyle: {
fontSize: 10
}
},
series: [
{
name: '工单统计',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 4,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false
},
emphasis: {
label: {
show: true,
fontSize: 12,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: [
{ value: orderStats.value.pending, name: '待接取', itemStyle: { color: '#909399' } },
{ value: orderStats.value.processing, name: '处理中', itemStyle: { color: '#E6A23C' } },
{ value: orderStats.value.reviewing, name: '待审批', itemStyle: { color: '#409EFF' } },
{ value: orderStats.value.completed, name: '已完成', itemStyle: { color: '#67C23A' } }
]
}
]
}
})
// 设备状态统计
const deviceStats = ref({
normal: 0,
fault: 0,
maintenance: 0,
scrapped: 0
})
// 图表配置
const chartOption = computed(() => {
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: ['正常', '故障', '维护中', '报废'],
axisTick: {
alignWithLabel: true
}
},
yAxis: {
type: 'value',
minInterval: 1 // 确保y轴刻度为整数
},
series: [
{
name: '设备数量',
type: 'bar',
barWidth: '60%',
data: [
{
value: deviceStats.value.normal,
itemStyle: { color: '#67C23A' }
},
{
value: deviceStats.value.fault,
itemStyle: { color: '#F56C6C' }
},
{
value: deviceStats.value.maintenance,
itemStyle: { color: '#E6A23C' }
},
{
value: deviceStats.value.scrapped,
itemStyle: { color: '#909399' }
}
],
label: {
show: true,
position: 'top',
formatter: '{c}'
}
}
]
}
})
// 监听设备统计数据变化,更新图表
watch(deviceStats, () => {
// 数据变化时图表会自动更新
}, { deep: true })
// 获取设备状态统计
const fetchDeviceStats = async () => {
try {
const res = await getDeviceStats()
if (res.code === 200) {
deviceStats.value = res.data || {
normal: 0,
fault: 0,
maintenance: 0,
scrapped: 0
}
} else {
console.error('获取设备统计失败:', res.msg)
}
} catch (error) {
console.error('获取设备统计错误:', error)
}
}
// 故障列表相关
const faultList = ref([])
const deviceList = ref([])
const dialogVisible = ref(false)
const faultForm = ref({
deviceId: '',
faultDesc: '',
reportType: '自主报修',
reportedId: '',
reportedName: '',
reportedPhone: ''
})
// 获取故障列表
const fetchFaultList = async () => {
try {
const res = await getFaultList()
if (res.code === 200) {
faultList.value = res.data || []
} else {
ElMessage.error(res.msg || '获取故障列表失败')
}
} catch (error) {
console.error('获取故障列表错误:', error)
ElMessage.error('获取故障列表失败')
}
}
// 获取设备列表
const fetchDeviceList = async () => {
try {
const res = await getDeviceList()
if (res.code === 200) {
deviceList.value = res.data || []
}
} catch (error) {
console.error('获取设备列表失败:', error)
}
}
// 获取状态标签类型
const getStatusType = (status) => {
const statusMap = {
'待处理': 'info',
'处理中': 'warning',
'进行中': 'warning',
'已解决': 'success',
'已完成': 'success',
'待审批': 'warning',
'已关闭': ''
}
return statusMap[status] || ''
}
// 设置默认用户信息
const setDefaultUserInfo = () => {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
faultForm.value.reportedId = userInfo.id || ''
faultForm.value.reportedName = userInfo.username || ''
faultForm.value.reportedPhone = userInfo.phone || ''
}
// 显示添加故障报修对话框
const showAddFaultDialog = () => {
// 重置表单
faultForm.value = {
deviceId: '',
faultDesc: '',
reportType: '自主报修',
reportedId: '',
reportedName: '',
reportedPhone: ''
}
// 设置用户信息
setDefaultUserInfo()
dialogVisible.value = true
}
// 提交报修
const handleSubmit = async () => {
try {
const res = await addDeviceFault(faultForm.value)
if (res.code === 200) {
ElMessage.success('报修成功')
dialogVisible.value = false
// 刷新故障列表
fetchFaultList()
} else {
ElMessage.error(res.msg || '报修失败')
}
} catch (error) {
console.error('报修失败:', error)
ElMessage.error('报修失败')
}
}
const currentOrderType = ref('inspection')
const currentOrders = ref([])
// API 映射对象
const apiMap = {
inspection: {
getAllOrders: inspectionOrderApi.getAllOrders,
updateOrder: inspectionOrderApi.updateOrder
},
maintenance: {
getAllOrders: maintenanceOrderApi.getAllOrders,
updateOrder: maintenanceOrderApi.updateOrder
},
repair: {
getAllOrders: repairOrderApi.getRepairOrderList,
updateOrder: repairOrderApi.updateRepairOrder
},
testing: {
getAllOrders: testingOrderApi.getAllOrders,
updateOrder: testingOrderApi.updateOrder
}
}
// 获取工单类型标签
const getOrderTypeLabel = (type) => {
const typeMap = {
'inspection': '巡检工单',
'maintenance': '保养工单',
'repair': '维修工单',
'testing': '检测工单'
}
return typeMap[type] || '工单'
}
// 处理工单类型变更
const handleOrderTypeChange = () => {
fetchOrders()
}
// 获取工单列表
const fetchOrders = async () => {
try {
if (currentOrderType.value === 'repair') {
// 维修工单的处理逻辑保持不变
const response = await getFaultList()
if (response.code === 200) {
const pendingFaults = (response.data.records || response.data).filter(
fault => fault.status === '待处理'
)
// 获取每个故障对应的设备信息
const ordersWithDeviceInfo = await Promise.all(
pendingFaults.map(async (fault) => {
const deviceResponse = await getDevice(fault.deviceId)
const deviceInfo = deviceResponse.code === 200 ? deviceResponse.data : null
return {
orderId: fault.faultId,
deviceId: deviceInfo?.deviceId || fault.deviceId,
deviceType: deviceInfo?.deviceType || '未知',
location: deviceInfo?.location || '未知',
faultId: fault.faultId,
status: fault.status,
faultDesc: fault.faultDesc
}
})
)
currentOrders.value = ordersWithDeviceInfo
} else {
ElMessage.error('获取故障数据失败')
}
return
}
// 其他工单类型
const api = apiMap[currentOrderType.value]
const response = await api.getAllOrders()
if (response.code === 200) {
const pendingOrders = (response.data.records || response.data).filter(
order => order.status === '待处理'
)
// 如果是巡检工单、保养工单或检测工单,需要获取设备信息
if (currentOrderType.value === 'inspection' || currentOrderType.value === 'maintenance' || currentOrderType.value === 'testing') {
// 获取每个工单对应的设备信息
const ordersWithDeviceInfo = await Promise.all(
pendingOrders.map(async (order) => {
// 根据工单类型选择不同的API获取计划信息
let planResponse;
if (currentOrderType.value === 'inspection') {
planResponse = await getPlanById(order.planId);
} else if (currentOrderType.value === 'maintenance') {
planResponse = await maintenancePlanApi.getPlanById(order.planId);
} else { // testing
planResponse = await testingPlanApi.getPlanById(order.planId);
}
const planInfo = planResponse.code === 200 ? planResponse.data : null
const deviceId = planInfo?.deviceId
// 再通过device_id获取设备信息
let deviceInfo = null
if (deviceId) {
const deviceResponse = await getDevice(deviceId)
deviceInfo = deviceResponse.code === 200 ? deviceResponse.data : null
}
return {
...order,
deviceId: deviceInfo?.deviceId || '未知',
deviceType: deviceInfo?.deviceType || '未知',
location: deviceInfo?.location || '未知'
}
})
)
currentOrders.value = ordersWithDeviceInfo
} else {
currentOrders.value = pendingOrders
}
} else {
ElMessage.error('获取工单数据失败')
}
} catch (error) {
console.error('获取工单数据错误:', error)
ElMessage.error('获取工单数据失败')
}
}
// 处理接取工单
const handleTakeOrder = async (order) => {
try {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
// 获取安全须知内容
const safetyNotice = `一、安全防护
wear防护装备:工作服、手套、安全帽、防护眼镜等。
设备断电锁定:切断电源,锁定设备,挂警示牌。
环境安全:保持工作区域整洁,高处作业系安全带。
二、维修前准备
工具检查:确保工具齐全、绝缘良好。
备件准备:确认规格匹配,准备备用件。
查阅资料:熟悉设备结构和维修要点。
三、维修过程
故障诊断:观察现象,记录症状,逐步排查。
操作规范:按手册操作,标记拆卸部件。
数据备份:备份重要数据,确保安全。
清洁维护:清理灰尘,检查润滑部件。
四、维修后测试
功能测试:启动设备,检查功能是否正常。
性能检查:确认运行参数正常。
安全检查:检查外壳、防护装置是否牢固。
五、记录与总结
记录信息:记录设备信息、维修过程、测试结果。
总结经验:分析故障原因,提出改进措施。
六、其他注意
遵守制度:严格按流程操作,妥善处理废弃物。`
if (currentOrderType.value === 'repair') {
const repairOrderData = {
faultId: order.faultId,
engineerId: userInfo.id,
repairDesc: '',
status: '处理中',
safetyNotice: safetyNotice
}
const res = await createRepairOrder(repairOrderData)
if (res.code === 200) {
// 更新故障状态为处理中
const updateFaultRes = await updateFaultStatus({
faultId: order.faultId,
status: '处理中'
})
if (updateFaultRes.code === 200) {
// 更新设备状态为维护中
try {
const updateDeviceRes = await updateDeviceStatus({
deviceId: order.deviceId,
status: '维护中'
})
if (updateDeviceRes.code === 200) {
ElMessage.success('维修工单接取成功')
// 刷新数据
fetchOrders()
fetchDeviceStats()
} else {
ElMessage.warning('工单已接取,但设备状态更新失败')
}
} catch (error) {
console.error('更新设备状态失败:', error)
ElMessage.warning('工单已接取,但设备状态更新失败')
}
} else {
ElMessage.warning('工单已接取,但故障状态更新失败')
}
} else {
ElMessage.error(res.msg || '接取工单失败')
}
return
}
// 其他工单类型的处理
const api = apiMap[currentOrderType.value]
const orderData = {
...order,
engineerId: userInfo.id,
status: '处理中'
}
const res = await api.updateOrder(orderData)
if (res.code === 200) {
ElMessage.success('工单接取成功')
fetchOrders()
} else {
ElMessage.error(res.msg || '接取工单失败')
}
} catch (error) {
console.error('接取工单错误:', error)
ElMessage.error('接取工单失败')
}
}
onMounted(() => {
// 从localStorage获取用户信息
const storedUserInfo = localStorage.getItem('userInfo')
if (storedUserInfo) {
userInfo.value = JSON.parse(storedUserInfo)
}
// 获取数据
fetchFaultList()
fetchDeviceList()
fetchOrders()
fetchDeviceStats()
fetchOrderStats() // 添加获取工单统计数据
})
// 获取工单统计数据
const fetchOrderStats = async () => {
// 如果不是工程师或管理员,则不需要获取工单统计
if (userRole.value !== 'engineer' && userRole.value !== 'admin' && userRole.value !== 'super_admin') return
try {
// 获取所有类型的工单
const [inspectionRes, maintenanceRes, testingRes, repairRes] = await Promise.all([
inspectionOrderApi.getAllOrders(),
maintenanceOrderApi.getAllOrders(),
testingOrderApi.getAllOrders(),
repairOrderApi.getRepairOrderList()
])
// 重置统计数据
orderStats.value = {
pending: 0,
processing: 0,
reviewing: 0,
completed: 0
}
// 获取当前用户ID
const currentUserId = userInfo.value.id
// 处理巡检工单
if (inspectionRes.code === 200) {
const allOrders = inspectionRes.data.records || inspectionRes.data || []
// 如果是工程师,只统计自己的工单;如果是管理员,统计所有工单
const orders = userRole.value === 'engineer'
? allOrders.filter(order => order.engineerId === currentUserId)
: allOrders
countOrdersByStatus(orders)
}
// 处理保养工单
if (maintenanceRes.code === 200) {
const allOrders = maintenanceRes.data.records || maintenanceRes.data || []
const orders = userRole.value === 'engineer'
? allOrders.filter(order => order.engineerId === currentUserId)
: allOrders
countOrdersByStatus(orders)
}
// 处理检测工单
if (testingRes.code === 200) {
const allOrders = testingRes.data.records || testingRes.data || []
const orders = userRole.value === 'engineer'
? allOrders.filter(order => order.engineerId === currentUserId)
: allOrders
countOrdersByStatus(orders)
}
// 处理维修工单
if (repairRes.code === 200) {
const allOrders = repairRes.data.records || repairRes.data || []
const orders = userRole.value === 'engineer'
? allOrders.filter(order => order.engineerId === currentUserId)
: allOrders
countOrdersByStatus(orders)
}
console.log(userRole.value === 'engineer' ? '当前工程师工单统计:' : '全部工单统计:', orderStats.value)
} catch (error) {
console.error('获取工单统计数据错误:', error)
}
}
// 统计不同状态的工单数量
const countOrdersByStatus = (orders) => {
orders.forEach(order => {
if (order.status === '待处理') {
orderStats.value.pending++
} else if (order.status === '处理中' || order.status === '进行中') {
orderStats.value.processing++
} else if (order.status === '待审批') {
orderStats.value.reviewing++
} else if (order.status === '已完成' || order.status === '已解决') {
orderStats.value.completed++
}
})
}
</script>
<style scoped>
.home-container {
padding: 20px;
}
.user-card {
height: 100%;
}
.user-info {
text-align: center;
padding: 10px 0;
}
.user-info h3 {
margin: 10px 0 5px;
}
.user-info p {
margin: 5px 0;
color: #666;
}
.stat-item h4 {
margin-bottom: 5px;
font-weight: normal;
color: #666;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.task-container {
height: 350px;
overflow-y: auto;
}
.task-list {
display: flex;
flex-direction: column;
gap: 15px;
padding-right: 5px;
}
/* 自定义滚动条样式 */
.task-container::-webkit-scrollbar {
width: 6px;
}
.task-container::-webkit-scrollbar-thumb {
background-color: #dcdfe6;
border-radius: 3px;
}
.task-container::-webkit-scrollbar-track {
background-color: #f5f7fa;
}
.task-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background-color: #f8f9fa;
border-radius: 4px;
}
.task-info h4 {
margin: 0 0 10px 0;
font-size: 16px;
color: #333;
}
.task-info p {
margin: 5px 0;
color: #666;
}
.task-action {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
}
.task-action .el-button {
margin-left: 0;
}
.status-box h3 {
margin: 0 0 10px 0;
font-size: 16px;
}
.status-box p {
margin: 0;
font-size: 24px;
font-weight: bold;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.table-container {
width: 100%;
overflow: hidden;
}
/* 添加图表容器样式 */
.chart-container {
width: 100%;
height: 300px;
}
.chart {
width: 100%;
height: 100%;
}
.engineer-stats {
margin-top: 10px;
}
.engineer-stats h4 {
text-align: center;
margin-bottom: 10px;
color: #333;
}
.pie-chart-container {
width: 100%;
height: 200px;
}
.pie-chart {
width: 100%;
height: 100%;
}
</style>
MaintenanceDetail.vue:
<template>
<div class="maintenance-detail-container">
<el-card class="maintenance-card">
<template #header>
<div class="card-header">
<span>保养工单详情</span>
<el-button @click="$router.back()">返回</el-button>
</div>
</template>
<!-- 添加步骤条 -->
<el-card class="steps-card" shadow="hover">
<el-steps :active="getActiveStep(orderDetail.status)" finish-status="success" align-center>
<el-step title="待处理" description="工单已创建"></el-step>
<el-step title="处理中" description="工程师处理中"></el-step>
<el-step title="待审批" description="等待审核"></el-step>
<el-step title="已完成" description="工单已完成"></el-step>
</el-steps>
</el-card>
<el-descriptions :column="1" border>
<el-descriptions-item label="工单号">{{ orderDetail.orderId }}</el-descriptions-item>
<el-descriptions-item label="计划编号">{{ orderDetail.planId }}</el-descriptions-item>
<el-descriptions-item label="设备ID">{{ deviceInfo.deviceId }}</el-descriptions-item>
<el-descriptions-item label="设备类型">{{ deviceInfo.deviceType }}</el-descriptions-item>
<el-descriptions-item label="设备位置">{{ deviceInfo.location }}</el-descriptions-item>
<el-descriptions-item label="计划时间">{{ formatDateTime(orderDetail.planTime) }}</el-descriptions-item>
<el-descriptions-item label="保养描述">
<el-input
v-model="orderDetail.maintenanceDesc"
type="textarea"
:rows="3"
placeholder="请输入保养描述"
:disabled="orderDetail.status === '已完成' || orderDetail.status === '待审批'"
/>
</el-descriptions-item>
<el-descriptions-item label="保养前照片">
<div class="upload-section">
<input
type="file"
ref="beforeFileInput"
style="display: none"
accept="image/*"
@change="(e) => handleFileChange(e, 'before')"
/>
<div
class="upload-area"
@click="() => triggerFileInput('before')"
@dragover.prevent
@drop.prevent="(e) => handleDrop(e, 'before')"
>
<template v-if="beforeFileList.length === 0">
<el-icon class="upload-icon"><Upload /></el-icon>
<div class="upload-text">
<span>点击选择或拖拽图片到此处</span>
<p>支持 jpg、png 格式图片</p>
</div>
</template>
<div v-else class="image-preview-container">
<div v-for="(img, index) in beforeFileList" :key="index" class="image-preview-item">
<el-image :src="img.url" fit="cover" />
<div class="image-actions">
<el-button
type="danger"
icon="Delete"
circle
@click.stop="() => removeImage(index, 'before')"
:disabled="orderDetail.status === '已完成' || orderDetail.status === '待审批'"
/>
</div>
</div>
</div>
</div>
</div>
</el-descriptions-item>
<el-descriptions-item label="保养后照片">
<div class="upload-section">
<input
type="file"
ref="afterFileInput"
style="display: none"
accept="image/*"
@change="(e) => handleFileChange(e, 'after')"
/>
<div
class="upload-area"
@click="() => triggerFileInput('after')"
@dragover.prevent
@drop.prevent="(e) => handleDrop(e, 'after')"
>
<template v-if="afterFileList.length === 0">
<el-icon class="upload-icon"><Upload /></el-icon>
<div class="upload-text">
<span>点击选择或拖拽图片到此处</span>
<p>支持 jpg、png 格式图片</p>
</div>
</template>
<div v-else class="image-preview-container">
<div v-for="(img, index) in afterFileList" :key="index" class="image-preview-item">
<el-image :src="img.url" fit="cover" />
<div class="image-actions">
<el-button
type="danger"
icon="Delete"
circle
@click.stop="() => removeImage(index, 'after')"
:disabled="orderDetail.status === '已完成' || orderDetail.status === '待审批'"
/>
</div>
</div>
</div>
</div>
</div>
</el-descriptions-item>
</el-descriptions>
<div class="action-buttons">
<el-button
type="primary"
@click="handleSave"
style="margin-right: 10px"
:disabled="orderDetail.status === '已完成' || orderDetail.status === '待审批' || !canOperate"
>{{ !canOperate ? '请等待3秒' : '保存' }}</el-button>
<el-button
type="success"
@click="handleComplete"
:disabled="orderDetail.status === '已完成' || orderDetail.status === '待审批' || !canOperate"
>{{ !canOperate ? '请等待3秒' : '完成工单' }}</el-button>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Upload } from '@element-plus/icons-vue'
import { getOrderById, updateOrder, updateOrderStatus } from '@/api/maintenanceOrder'
import { getDevice } from '@/api/device'
import { getPlanById } from '@/api/maintenance'
const route = useRoute()
const router = useRouter()
const orderDetail = ref({})
const deviceInfo = ref({})
const beforeFileList = ref([])
const afterFileList = ref([])
const beforeFileInput = ref(null)
const afterFileInput = ref(null)
const canOperate = ref(false)
const watermarkColor = ref('#000000')
// 格式化日期时间
const formatDateTime = (dateTime) => {
if (!dateTime) return ''
const date = new Date(dateTime)
return date.toLocaleString('zh-CN')
}
// 获取当前步骤
const getActiveStep = (status) => {
const stepMap = {
'pending': 0,
'processing': 1,
'waiting_approval': 2,
'completed': 3,
'待处理': 0,
'处理中': 1,
'待审批': 2,
'已完成': 3
}
return stepMap[status] || 0
}
// 获取工单详情
const fetchOrderDetail = async () => {
try {
const orderId = route.params.id
const response = await getOrderById(orderId)
if (response.code === 200 && response.data) {
orderDetail.value = response.data
// 获取计划信息
const planResponse = await getPlanById(response.data.planId)
if (planResponse.code === 200 && planResponse.data) {
// 获取设备信息
const deviceResponse = await getDevice(planResponse.data.deviceId)
if (deviceResponse.code === 200) {
deviceInfo.value = deviceResponse.data
}
}
// 处理保养前照片数据 - 修改这部分代码
if (orderDetail.value.beforePhotos) {
try {
// 清空现有图片列表
beforeFileList.value = []
// 将逗号分隔的字符串转换为数组,并过滤掉空字符串
const photoArray = orderDetail.value.beforePhotos.split(',').filter(item => item.trim() !== '')
// 处理每张照片
beforeFileList.value = photoArray.map((base64String, index) => {
// 检查并处理 Base64 字符串
let imgSrc = base64String
if (base64String && !base64String.startsWith('data:image')) {
imgSrc = 'data:image/png;base64,' + base64String
}
return {
url: imgSrc,
name: `before_photo_${index}`
}
}).filter(item => {
// 验证 Base64 数据的有效性
return item.url && item.url.length > 22 // 最小有效base64长度检查
})
} catch (error) {
console.error('处理保养前照片数据失败:', error)
ElMessage.warning('部分保养前照片加载失败')
beforeFileList.value = [] // 出错时清空列表
}
} else {
beforeFileList.value = [] // 没有照片时确保列表为空
}
// 处理保养后照片数据 - 修改这部分代码
if (orderDetail.value.afterPhotos) {
try {
// 清空现有图片列表
afterFileList.value = []
// 将逗号分隔的字符串转换为数组,并过滤掉空字符串
const photoArray = orderDetail.value.afterPhotos.split(',').filter(item => item.trim() !== '')
// 处理每张照片
afterFileList.value = photoArray.map((base64String, index) => {
// 检查并处理 Base64 字符串
let imgSrc = base64String
if (base64String && !base64String.startsWith('data:image')) {
imgSrc = 'data:image/png;base64,' + base64String
}
return {
url: imgSrc,
name: `after_photo_${index}`
}
}).filter(item => {
// 验证 Base64 数据的有效性
return item.url && item.url.length > 22 // 最小有效base64长度检查
})
} catch (error) {
console.error('处理保养后照片数据失败:', error)
ElMessage.warning('部分保养后照片加载失败')
afterFileList.value = [] // 出错时清空列表
}
} else {
afterFileList.value = [] // 没有照片时确保列表为空
}
}
} catch (error) {
console.error('获取工单详情失败:', error)
ElMessage.error('获取工单详情失败')
}
}
// 处理完成工单
const handleComplete = async () => {
try {
// 先保存当前工单信息
await handleSave()
// 更新工单状态为待审批
await updateOrderStatus(orderDetail.value.orderId, '待审批')
ElMessage.success('工单已提交审批')
orderDetail.value.status = '待审批'
} catch (error) {
console.error('完成工单失败:', error)
ElMessage.error('完成工单失败')
}
}
// 处理保存
const handleSave = async () => {
try {
const beforeImageUrls = beforeFileList.value
.filter(file => file.url && file.url.length > 22)
.map(file => file.url)
const afterImageUrls = afterFileList.value
.filter(file => file.url && file.url.length > 22)
.map(file => file.url)
const updateData = {
...orderDetail.value,
beforePhotos: beforeImageUrls.join(','),
afterPhotos: afterImageUrls.join(',')
}
const response = await updateOrder(updateData)
if (response.code === 200) {
ElMessage.success('保存成功')
} else {
throw new Error(response.msg || '保存失败')
}
} catch (error) {
console.error('保存失败:', error)
ElMessage.error('保存失败:' + error.message)
}
}
// 触发文件选择
const triggerFileInput = (type) => {
if (orderDetail.value.status === '已完成' || orderDetail.value.status === '待审批') return
if (type === 'before') {
beforeFileInput.value.click()
} else {
afterFileInput.value.click()
}
}
// 处理文件选择
const handleFileChange = (e, type) => {
const file = e.target.files[0]
if (file) {
processImage(file, type)
}
}
// 处理拖拽
const handleDrop = (e, type) => {
if (orderDetail.value.status === '已完成' || orderDetail.value.status === '待审批') return
const file = e.dataTransfer.files[0]
if (file && file.type.startsWith('image/')) {
processImage(file, type)
} else {
ElMessage.warning('请上传图片文件')
}
}
// 处理图片
// 在 script setup 顶部添加 location ref
const location = ref('未知位置')
// 添加 getLocation 函数
const getLocation = () => {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
location.value = '未知位置'
resolve(location.value)
return
}
navigator.geolocation.getCurrentPosition(
async (position) => {
try {
const { longitude, latitude } = position.coords
const response = await fetch(
`https://restapi.amap.com/v3/geocode/regeo?key=117ac94e50c9c03e7360334a3b84a5f7&location=${longitude},${latitude}&extensions=base`
)
const data = await response.json()
if (data.status === '1' && data.regeocode) {
location.value = data.regeocode.formatted_address
} else {
location.value = `${longitude},${latitude}`
}
resolve(location.value)
} catch (error) {
console.error('地理编码转换失败:', error)
location.value = '未知位置'
resolve(location.value)
}
},
(error) => {
console.error('获取位置失败:', error)
location.value = '未知位置'
resolve(location.value)
},
{
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
}
)
})
}
// 修改 processImage 函数
const processImage = async (file, type) => {
try {
// 先获取位置信息
await getLocation()
} catch (error) {
console.error('获取位置失败:', error)
}
const reader = new FileReader()
reader.onload = (e) => {
const img = new Image()
img.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext('2d')
// 绘制原图
ctx.drawImage(img, 0, 0)
// 设置水印样式
ctx.font = '24px Arial'
ctx.fillStyle = watermarkColor.value
ctx.globalAlpha = 0.5
ctx.textAlign = 'right' // 设置文本右对齐
// 添加位置和时间水印
const locationText = `拍摄地点:${location.value}`
const timeText = new Date().toLocaleString('zh-CN')
const x = canvas.width - 20 // 距离右边界20像素
const locationY = canvas.height - 50
const timeY = canvas.height - 20
// 绘制水印
ctx.fillText(locationText, x, locationY)
ctx.fillText(timeText, x, timeY)
const watermarkedImage = canvas.toDataURL(file.type)
if (type === 'before') {
beforeFileList.value.push({
url: watermarkedImage,
name: file.name
})
} else {
afterFileList.value.push({
url: watermarkedImage,
name: file.name
})
}
}
img.src = e.target.result
}
reader.readAsDataURL(file)
}
// 移除图片
const removeImage = (index, type) => {
if (orderDetail.value.status === '已完成' || orderDetail.value.status === '待审批') return
if (type === 'before') {
beforeFileList.value.splice(index, 1)
} else {
afterFileList.value.splice(index, 1)
}
}
onMounted(() => {
fetchOrderDetail()
setTimeout(() => {
canOperate.value = true
}, 3000)
})
</script>
<style scoped>
.maintenance-detail-container {
padding: 20px;
}
.steps-card {
margin-bottom: 20px;
}
.maintenance-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.action-buttons {
margin-top: 20px;
text-align: center;
}
.upload-section {
padding: 10px;
width: 100%;
box-sizing: border-box;
overflow: hidden; /* 添加此行 */
}
.upload-area {
width: 100%;
min-height: 180px;
border: 2px dashed #d9d9d9;
border-radius: 8px;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: border-color 0.3s;
padding: 20px;
box-sizing: border-box; /* 添加此行 */
}
.image-preview-container {
width: 100%;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 16px;
padding: 10px;
box-sizing: border-box; /* 添加此行 */
}
.image-preview-item {
position: relative;
aspect-ratio: 1;
border-radius: 4px;
overflow: hidden;
}
.image-preview-item .el-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.image-actions {
position: absolute;
top: 8px;
right: 8px;
display: none;
}
.image-preview-item:hover .image-actions {
display: block;
}
</style>

浙公网安备 33010602011771号