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>
posted @ 2025-04-29 11:36  三拍  阅读(19)  评论(0)    收藏  举报