三拍:巡检工单
InspectionOrderController
package com.example.demo.controller;
import com.example.demo.entity.InspectionOrder;
import com.example.demo.service.InspectionOrderService;
import com.example.demo.common.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/inspection-order")
public class InspectionOrderController {
@Autowired
private InspectionOrderService inspectionOrderService;
@GetMapping("/list")
public Result getAll() {
return Result.success(inspectionOrderService.getAllOrders());
}
@GetMapping("/{id}")
public Result getById(@PathVariable("id") String orderId) {
return Result.success(inspectionOrderService.getOrderById(orderId));
}
@PostMapping("/add")
public Result add(@RequestBody InspectionOrder order) {
return inspectionOrderService.addOrder(order)
? Result.success()
: Result.error("添加工单失败");
}
@PutMapping("/update")
public Result update(@RequestBody InspectionOrder order) {
return inspectionOrderService.updateOrder(order)
? Result.success()
: Result.error("更新工单失败");
}
@DeleteMapping("/{id}")
public Result delete(@PathVariable("id") String orderId) {
return inspectionOrderService.deleteOrder(orderId)
? Result.success()
: Result.error("删除工单失败");
}
@PutMapping("/status")
public Result updateStatus(@RequestParam String orderId, @RequestParam String status) {
return inspectionOrderService.updateOrderStatus(orderId, status)
? Result.success()
: Result.error("状态更新失败");
}
@PostMapping("/generate")
public Result generateOrders(@RequestBody Map<String, List<Integer>> request) {
List<Integer> planIds = request.get("planIds");
return inspectionOrderService.generateOrdersForPlans(planIds)
? Result.success()
: Result.error("工单生成失败");
}
}
InspectionOrder
package com.example.demo.entity;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class InspectionOrder {
private String orderId;
private Integer planId;
private String engineerId;
private LocalDateTime planTime;
private String inspectionDesc;
private String status;
private String photos;
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public Integer getPlanId() {
return planId;
}
public void setPlanId(Integer planId) {
this.planId = planId;
}
public String getEngineerId() {
return engineerId;
}
public void setEngineerId(String engineerId) {
this.engineerId = engineerId;
}
public LocalDateTime getPlanTime() {
return planTime;
}
public void setPlanTime(LocalDateTime planTime) {
this.planTime = planTime;
}
public String getInspectionDesc() {
return inspectionDesc;
}
public void setInspectionDesc(String inspectionDesc) {
this.inspectionDesc = inspectionDesc;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getPhotos() {
return photos;
}
public void setPhotos(String photos) {
this.photos = photos;
}
}
InspectionOrderServiceImpl
package com.example.demo.service.impl;
import com.example.demo.entity.InspectionOrder;
import com.example.demo.mapper.InspectionOrderMapper;
import com.example.demo.service.InspectionOrderService;
import com.example.demo.utils.OrderIdGenerator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class InspectionOrderServiceImpl implements InspectionOrderService {
@Autowired
private InspectionOrderMapper inspectionOrderMapper;
@Override
public List<InspectionOrder> getAllOrders() {
try {
return inspectionOrderMapper.selectAll();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
public InspectionOrder getOrderById(String orderId) {
try {
return inspectionOrderMapper.selectById(orderId);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
@Transactional
public boolean addOrder(InspectionOrder order) {
try {
// 生成工单号
order.setOrderId(OrderIdGenerator.generateOrderId());
// 设置默认状态
if (order.getStatus() == null) {
order.setStatus("待处理");
}
// 设置创建时间
if (order.getPlanTime() == null) {
order.setPlanTime(LocalDateTime.now());
}
return inspectionOrderMapper.insert(order) > 0;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
@Transactional
public boolean updateOrder(InspectionOrder order) {
try {
if (order.getOrderId() == null) {
return false;
}
return inspectionOrderMapper.update(order) > 0;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
@Transactional
public boolean deleteOrder(String orderId) {
try {
return inspectionOrderMapper.deleteById(orderId) > 0;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
@Transactional
public boolean updateOrderStatus(String orderId, String status) {
try {
if (!isValidStatus(status)) {
return false;
}
return inspectionOrderMapper.updateStatus(orderId, status) > 0;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Transactional
public boolean generateOrdersForPlans(List<Integer> planIds) {
try {
for (Integer planId : planIds) {
InspectionOrder order = new InspectionOrder();
order.setPlanId(planId);
order.setPlanTime(LocalDateTime.now());
order.setStatus("待处理");
// 生成工单
if (!addOrder(order)) {
throw new RuntimeException("工单生成失败");
}
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
private boolean isValidStatus(String status) {
if (status == null) {
return false;
}
String[] validStatuses = {"待处理", "处理中", "已完成", "待审批", "已关闭"};
for (String validStatus : validStatuses) {
if (validStatus.equals(status)) {
return true;
}
}
return false;
}
}
InspectionOrderService
package com.example.demo.service;
import com.example.demo.entity.InspectionOrder;
import java.util.List;
public interface InspectionOrderService {
List<InspectionOrder> getAllOrders();
InspectionOrder getOrderById(String orderId);
boolean addOrder(InspectionOrder order);
boolean updateOrder(InspectionOrder order);
boolean deleteOrder(String orderId);
boolean updateOrderStatus(String orderId, String status);
boolean generateOrdersForPlans(List<Integer> planIds);
}
InspectionDetail.vue
<template>
<div class="inspection-detail-container">
<el-card class="inspection-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.inspectionDesc"
type="textarea"
:rows="3"
placeholder="请输入巡检描述"
:disabled="orderDetail.status === 'completed'"
/>
</el-descriptions-item>
<el-descriptions-item label="巡检照片">
<div class="upload-section">
<input
type="file"
ref="fileInput"
style="display: none"
accept="image/*"
@change="handleFileChange"
/>
<div
class="upload-area"
@click="triggerFileInput"
@dragover.prevent
@drop.prevent="handleDrop"
>
<template v-if="fileList.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 fileList" :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)"
:disabled="orderDetail.status === 'completed'"
/>
</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 === 'completed' || !canOperate"
>{{ !canOperate ? '请等待3秒' : '保存' }}</el-button>
<el-button
type="success"
@click="handleComplete"
:disabled="orderDetail.status === 'completed' || !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/inspectionOrder'
import { getDevice } from '@/api/device'
import { getPlanById } from '@/api/inspection'
const route = useRoute()
const router = useRouter()
const orderDetail = ref({})
const deviceInfo = ref({})
const fileList = ref([])
const fileInput = 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.photos) {
try {
// 清空现有图片列表
fileList.value = []
// 将逗号分隔的字符串转换为数组,并过滤掉空字符串
const photoArray = orderDetail.value.photos.split(',').filter(item => item.trim() !== '')
// 处理每张照片
fileList.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: `photo_${index}` // 使用索引而不是时间戳
}
}).filter(item => {
// 验证 Base64 数据的有效性
return item.url && item.url.length > 22 // 最小有效base64长度检查
})
} catch (error) {
console.error('处理照片数据失败:', error)
ElMessage.warning('部分照片加载失败')
fileList.value = [] // 出错时清空列表
}
} else {
fileList.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 {
// 将图片转换为Base64字符串数组,过滤掉可能的无效数据
const imageUrls = fileList.value
.filter(file => file.url && file.url.length > 22) // 过滤无效图片
.map(file => file.url)
// 构建更新的工单数据
const updateData = {
...orderDetail.value,
photos: imageUrls.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 = () => {
if (orderDetail.value.status === 'completed') return
fileInput.value.click()
}
// 处理文件选择
const handleFileChange = (e) => {
const file = e.target.files[0]
if (file) {
processImage(file)
}
}
// 处理拖拽
const handleDrop = (e) => {
if (orderDetail.value.status === 'completed') return
const file = e.dataTransfer.files[0]
if (file && file.type.startsWith('image/')) {
processImage(file)
} else {
ElMessage.warning('请上传图片文件')
}
}
// 处理图片
const processImage = (file) => {
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)
// 添加水印
const text = new Date().toLocaleString('zh-CN')
ctx.font = '24px Arial'
ctx.fillStyle = watermarkColor.value
ctx.globalAlpha = 0.5
const textWidth = ctx.measureText(text).width
const x = canvas.width - textWidth - 20
const y = canvas.height - 20
ctx.fillText(text, x, y)
const watermarkedImage = canvas.toDataURL(file.type)
fileList.value.push({
url: watermarkedImage,
name: file.name
})
}
img.src = e.target.result
}
reader.readAsDataURL(file)
}
// 移除图片
const removeImage = (index) => {
if (orderDetail.value.status === 'completed') return
fileList.value.splice(index, 1)
}
onMounted(() => {
fetchOrderDetail()
setTimeout(() => {
canOperate.value = true
}, 3000)
})
</script>
<style scoped>
.inspection-detail-container {
padding: 20px;
}
.steps-card {
margin-bottom: 20px;
}
.inspection-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;
}
:deep(.el-descriptions__label) {
min-width: 120px;
width: 120px;
white-space: nowrap;
}
</style>
浙公网安备 33010602011771号