三拍:为照片添加地址水印

以维修工单为例这里调用高德地图的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>
posted @ 2025-05-19 11:11  QixunQiu  阅读(28)  评论(0)    收藏  举报