三拍:检测工单
TestingOrderController
package com.example.demo.controller;
import com.example.demo.entity.TestingOrder;
import com.example.demo.service.TestingOrderService;
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/testing-order")
public class TestingOrderController {
@Autowired
private TestingOrderService testingOrderService;
@GetMapping("/list")
public Result getAll() {
return Result.success(testingOrderService.getAllOrders());
}
@GetMapping("/{id}")
public Result getById(@PathVariable("id") String orderId) {
return Result.success(testingOrderService.getOrderById(orderId));
}
@PostMapping("/add")
public Result add(@RequestBody TestingOrder order) {
return testingOrderService.addOrder(order)
? Result.success()
: Result.error("添加工单失败");
}
@PutMapping("/update")
public Result update(@RequestBody TestingOrder order) {
return testingOrderService.updateOrder(order)
? Result.success()
: Result.error("更新工单失败");
}
@DeleteMapping("/{id}")
public Result delete(@PathVariable("id") String orderId) {
return testingOrderService.deleteOrder(orderId)
? Result.success()
: Result.error("删除工单失败");
}
@PutMapping("/status")
public Result updateStatus(@RequestParam String orderId, @RequestParam String status) {
return testingOrderService.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 testingOrderService.generateOrdersForPlans(planIds)
? Result.success()
: Result.error("工单生成失败");
}
}
TestingOrder
package com.example.demo.controller;
import com.example.demo.entity.TestingOrder;
import com.example.demo.service.TestingOrderService;
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/testing-order")
public class TestingOrderController {
@Autowired
private TestingOrderService testingOrderService;
@GetMapping("/list")
public Result getAll() {
return Result.success(testingOrderService.getAllOrders());
}
@GetMapping("/{id}")
public Result getById(@PathVariable("id") String orderId) {
return Result.success(testingOrderService.getOrderById(orderId));
}
@PostMapping("/add")
public Result add(@RequestBody TestingOrder order) {
return testingOrderService.addOrder(order)
? Result.success()
: Result.error("添加工单失败");
}
@PutMapping("/update")
public Result update(@RequestBody TestingOrder order) {
return testingOrderService.updateOrder(order)
? Result.success()
: Result.error("更新工单失败");
}
@DeleteMapping("/{id}")
public Result delete(@PathVariable("id") String orderId) {
return testingOrderService.deleteOrder(orderId)
? Result.success()
: Result.error("删除工单失败");
}
@PutMapping("/status")
public Result updateStatus(@RequestParam String orderId, @RequestParam String status) {
return testingOrderService.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 testingOrderService.generateOrdersForPlans(planIds)
? Result.success()
: Result.error("工单生成失败");
}
}
TestingOrderServiceImpl
package com.example.demo.service.impl;
import com.example.demo.entity.TestingOrder;
import com.example.demo.mapper.TestingOrderMapper;
import com.example.demo.service.TestingOrderService;
import com.example.demo.utils.TestingOrderIdGenerator;
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 TestingOrderServiceImpl implements TestingOrderService {
@Autowired
private TestingOrderMapper testingOrderMapper;
@Override
public List<TestingOrder> getAllOrders() {
try {
return testingOrderMapper.selectAll();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
public TestingOrder getOrderById(String orderId) {
try {
return testingOrderMapper.selectById(orderId);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
@Transactional
public boolean addOrder(TestingOrder order) {
try {
order.setOrderId(TestingOrderIdGenerator.generateOrderId());
if (order.getStatus() == null) {
order.setStatus("待处理");
}
if (order.getPlanTime() == null) {
order.setPlanTime(LocalDateTime.now());
}
return testingOrderMapper.insert(order) > 0;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
@Transactional
public boolean updateOrder(TestingOrder order) {
try {
if (order.getOrderId() == null) {
return false;
}
return testingOrderMapper.update(order) > 0;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
@Transactional
public boolean deleteOrder(String orderId) {
try {
return testingOrderMapper.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 testingOrderMapper.updateStatus(orderId, status) > 0;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Transactional
public boolean generateOrdersForPlans(List<Integer> planIds) {
try {
for (Integer planId : planIds) {
TestingOrder order = new TestingOrder();
order.setPlanId(planId);
order.setPlanTime(LocalDateTime.now());
order.setStatus("待处理");
order.setEngineerId(null);
order.setTestingDesc(null);
order.setPhotos(null);
// 生成工单
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;
}
}
TestingOrderService
package com.example.demo.service;
import com.example.demo.entity.TestingOrder;
import java.util.List;
public interface TestingOrderService {
List<TestingOrder> getAllOrders();
TestingOrder getOrderById(String orderId);
boolean addOrder(TestingOrder order);
boolean updateOrder(TestingOrder order);
boolean deleteOrder(String orderId);
boolean updateOrderStatus(String orderId, String status);
boolean generateOrdersForPlans(List<Integer> planIds);
}
TestingDetail.vue
<template>
<div class="testing-detail-container">
<el-card class="testing-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.testingDesc"
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="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 === '已完成' || 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/testingOrder'
import { getDevice } from '@/api/device'
import { getPlanById } from '@/api/testing'
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('获取工单详情失败')
}
}
// 修改 handleSave 函数
const handleSave = async () => {
try {
const imageUrls = fileList.value
.filter(file => file.url && file.url.length > 22)
.map(file => file.url)
const updateData = {
orderId: orderDetail.value.orderId,
planId: orderDetail.value.planId, // 保持原值
engineerId: orderDetail.value.engineerId, // 保持原值
planTime: orderDetail.value.planTime, // 保持原值
testingDesc: orderDetail.value.testingDesc,
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)
}
}
// 修改 handleComplete 函数
const handleComplete = async () => {
try {
const imageUrls = fileList.value
.filter(file => file.url && file.url.length > 22)
.map(file => file.url)
const updateData = {
orderId: orderDetail.value.orderId,
planId: orderDetail.value.planId, // 保持原值
engineerId: orderDetail.value.engineerId, // 保持原值
planTime: orderDetail.value.planTime, // 保持原值
testingDesc: orderDetail.value.testingDesc,
photos: imageUrls.join(',')
}
// 保存工单信息
const saveResponse = await updateOrder(updateData)
if (saveResponse.code !== 200) {
throw new Error(saveResponse.msg || '保存失败')
}
// 更新工单状态为待审批
const statusResponse = await updateOrderStatus(orderDetail.value.orderId, '待审批')
if (statusResponse.code === 200) {
ElMessage.success('工单已提交审批')
orderDetail.value.status = '待审批'
} else {
throw new Error(statusResponse.msg || '更新状态失败')
}
} catch (error) {
console.error('完成工单失败:', error)
ElMessage.error('完成工单失败:' + error.message)
}
}
// 触发文件选择
const triggerFileInput = () => {
if (orderDetail.value.status === '已完成' || orderDetail.value.status === '待审批') return
fileInput.value.click()
}
// 处理文件选择
const handleFileChange = (e) => {
const file = e.target.files[0]
if (file) {
processImage(file)
}
}
// 处理拖拽
const handleDrop = (e) => {
if (orderDetail.value.status === '已完成' || orderDetail.value.status === '待审批') 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 === '已完成' || orderDetail.value.status === '待审批') return
fileList.value.splice(index, 1)
}
onMounted(() => {
fetchOrderDetail()
setTimeout(() => {
canOperate.value = true
}, 3000)
})
</script>
<style scoped>
.testing-detail-container {
padding: 20px;
}
.steps-card {
margin-bottom: 20px;
}
.testing-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: 5px;
width: 100%;
box-sizing: border-box;
overflow: hidden; /* 防止内容溢出 */
}
.upload-area {
width: 100%;
min-height: 150px; /* 减小最小高度 */
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: 10px; /* 减小内边距 */
box-sizing: border-box; /* 确保padding计入总宽度 */
}
.upload-area:hover {
border-color: #409EFF;
}
.upload-icon {
font-size: 28px;
color: #8c939d;
margin-bottom: 8px;
}
.upload-text {
text-align: center;
color: #606266;
}
.upload-text p {
margin: 5px 0 0;
font-size: 12px;
}
/* 修改图片预览容器样式 */
.image-preview-container {
width: 100%;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); /* 减小图片尺寸 */
gap: 10px; /* 减小间距 */
padding: 5px;
box-sizing: border-box;
}
.image-preview-item {
position: relative;
aspect-ratio: 1;
border-radius: 4px;
overflow: hidden;
max-height: 120px; /* 限制最大高度 */
}
.image-preview-item .el-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.image-actions {
position: absolute;
top: 5px;
right: 5px;
display: none;
}
.image-preview-item:hover .image-actions {
display: block;
}
/* 添加描述项样式,确保表头宽度固定 */
:deep(.el-descriptions__label) {
min-width: 120px;
width: 120px;
}
:deep(.el-descriptions__content) {
overflow: auto; /* 允许内容滚动 */
}
/* 确保表格布局正确 */
:deep(.el-descriptions) {
width: 100%;
table-layout: fixed;
}
:deep(.el-descriptions__body) {
width: 100%;
}
:deep(.el-descriptions__table) {
width: 100%;
table-layout: fixed;
}
</style>
浙公网安备 33010602011771号