三拍:为照片添加地址水印
以维修工单为例这里调用高德地图的api(web服务)
Ps:不能使用Web js 服务这个只能显示ip坐标,无具体中文显示,可读性差
<template>
<div class="work-detail-container">
<!-- 添加音频元素 -->
<audio id="notice-audio" style="display: none;">
<source src="@/assets/repair_notice.mp3" type="audio/mpeg">
</audio>
<!-- 现有的模板内容 -->
<el-card class="work-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="设备ID">{{ orderDetail.deviceId }}</el-descriptions-item>
<el-descriptions-item label="故障描述">{{ orderDetail.faultDesc }}</el-descriptions-item>
<el-descriptions-item label="安全须知">
<pre class="safety-notice">{{ orderDetail.safetyNotice }}</pre>
</el-descriptions-item>
<el-descriptions-item label="维修描述">
<el-input
v-model="orderDetail.repairDesc"
type="textarea"
:rows="3"
placeholder="请输入维修描述"
/>
</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)" />
</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 === '已完成' || !canOperate"
>{{ !canOperate ? '请等待3秒' : '保存' }}</el-button>
<el-button
type="success"
@click="handleComplete"
:disabled="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 { getRepairOrderList, updateRepairOrderStatus, updateRepairOrder } from '@/api/repairOrder'
import { getFault, updateFaultStatus } from '@/api/deviceFault' // 添加 updateFaultStatus
// 在 script setup 顶部其他变量声明后添加
const route = useRoute()
const router = useRouter()
const orderDetail = ref({})
const fileList = ref([])
const canOperate = ref(false) // 添加这行
// 获取当前步骤
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 getRepairOrderList({ engineerId: JSON.parse(localStorage.getItem('userInfo') || '{}').userId })
const order = response.data.find(o => o.orderId === parseInt(orderId))
if (order) {
const faultResponse = await getFault(order.faultId)
orderDetail.value = {
...order,
deviceId: faultResponse.data.deviceId,
faultDesc: faultResponse.data.faultDesc
}
// 处理已有的照片数据
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) {
ElMessage.error('获取工单详情失败')
}
}
const handleComplete = async () => {
try {
// 先调用保存方法
await handleSave()
// 更新工单状态
await updateRepairOrderStatus({
orderId: orderDetail.value.orderId,
status: 'completed'
})
// 更新故障状态为待审批
await updateFaultStatus({
faultId: orderDetail.value.faultId,
status: '待审批'
})
ElMessage.success('工单已完成')
orderDetail.value.status = 'completed'
} catch (error) {
ElMessage.error('更新状态失败')
}
}
const handleUploadSuccess = (response, file) => {
ElMessage.success('上传成功')
if (!orderDetail.value.photos) {
orderDetail.value.photos = []
}
orderDetail.value.photos.push(response.url)
}
const handleUploadError = () => {
ElMessage.error('上传失败')
}
const fileInput = ref(null)
const watermarkText = ref('')
const watermarkPosition = ref('bottomRight')
const watermarkColor = ref('#000000')
// 触发文件选择
const triggerFileInput = () => {
fileInput.value.click()
}
// 添加拖拽处理函数
const handleDrop = (e) => {
const file = e.dataTransfer.files[0]
if (file && file.type.startsWith('image/')) {
processImage(file)
} else {
ElMessage.warning('请上传图片文件')
}
}
// 处理文件选择
const handleFileChange = (e) => {
const file = e.target.files[0]
if (file) {
processImage(file)
}
}
// 在 script setup 中添加获取位置的函数
// 在顶部变量声明中添加
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=api密钥&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
}
)
})
}
// 修改图片处理函数
const processImage = async (file) => {
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)
// 转换为 base64 并添加到列表
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) => {
fileList.value.splice(index, 1)
}
// 修改保存方法
const handleSave = async () => {
try {
// 将图片转换为Base64字符串数组,过滤掉可能的无效数据
const imageUrls = fileList.value
.filter(file => file.url && file.url.length > 22) // 过滤无效图片
.map(file => file.url)
// 构建更新的工单数据
const updateData = {
orderId: orderDetail.value.orderId,
faultId: orderDetail.value.faultId,
engineerId: orderDetail.value.engineerId,
repairDesc: orderDetail.value.repairDesc,
status: orderDetail.value.status,
safetyNotice: orderDetail.value.safetyNotice,
photos: imageUrls.join(',') // 将图片数组转换为逗号分隔的字符串
}
// 调用更新接口
const response = await updateRepairOrder(updateData)
if (response.code === 200) {
ElMessage.success('保存成功')
} else {
throw new Error(response.msg)
}
} catch (error) {
ElMessage.error('保存失败:' + error.message)
}
}
// 添加音频播放逻辑
const playNoticeAudio = () => {
const audio = document.getElementById('notice-audio')
function tryPlay() {
document.removeEventListener('click', tryPlay)
audio.play().then(() => {
console.log('提示音播放成功')
}).catch(err => {
console.error('提示音播放失败:', err)
})
}
// 页面第一次点击触发播放
document.addEventListener('click', tryPlay)
}
onMounted(() => {
// 在组件挂载后初始化音频播放
playNoticeAudio()
fetchOrderDetail()
// 添加3秒计时器
setTimeout(() => {
canOperate.value = true
}, 3000)
})
</script>
<style scoped>
.work-detail-container {
padding: 20px;
}
.steps-card {
margin-bottom: 20px;
}
.work-card {
margin-bottom: 20px;
}
/* 现有的模板内容 -->
.work-detail-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.action-buttons {
margin-top: 20px;
text-align: center;
}
.upload-demo {
margin: 10px 0;
}
.el-descriptions-item {
margin-bottom: 20px;
width: 100%; /* 确保描述项占满宽度 */
.upload-section {
padding: 10px; /* 减小内边距 */
width: 100%; /* 确保上传区域占满宽度 */
box-sizing: border-box; /* 包含内边距在宽度内 */
}
.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: 10px; /* 减小内边距 */
box-sizing: border-box; /* 包含内边距在宽度内 */
}
.image-preview-container {
width: 100%;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); /* 减小图片大小 */
gap: 10px; /* 减小间距 */
padding: 5px; /* 减小内边距 */
}
.watermark-settings {
margin: 20px 0;
display: flex;
align-items: center;
}
.image-list {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-top: 20px;
}
.image-item {
position: relative;
width: 200px;
height: 200px;
}
.image-item .el-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.image-item .el-button {
position: absolute;
top: 10px;
right: 10px;
}
.safety-notice {
white-space: pre-wrap;
word-wrap: break-word;
font-family: inherit;
margin: 0;
padding: 0;
line-height: 1.5;
}
.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;
}
.image-preview-container {
width: 100%;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 16px;
padding: 10px;
}
.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号