1538 lines
37 KiB
Vue
1538 lines
37 KiB
Vue
<template>
|
||
<view>
|
||
<!-- 主页面内容 -->
|
||
<view>
|
||
<image class="logo" src="/static/logo.jpg"></image>
|
||
<text class="title">{{ title }}</text>
|
||
<view class="btn-view">
|
||
<button type="primary" class="btn" @click="handleStartCapture">
|
||
开始拍摄
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 隐藏的canvas用于水印处理(仅App/小程序端使用) -->
|
||
<!-- #ifndef H5 -->
|
||
<canvas
|
||
canvas-id="watermarkCanvas"
|
||
style="
|
||
position: fixed;
|
||
top: -9999px;
|
||
left: -9999px;
|
||
width: 2000px;
|
||
height: 2000px;
|
||
"
|
||
></canvas>
|
||
<!-- #endif -->
|
||
|
||
<!-- 弹窗内容 -->
|
||
<uni-popup
|
||
ref="popup"
|
||
:is-mask-click="false"
|
||
type="bottom"
|
||
border-radius="0 0 0 0"
|
||
>
|
||
<view class="container">
|
||
<view style="height: 80rpx"></view>
|
||
|
||
<!-- 图片列表区域 -->
|
||
<view class="preview-box">
|
||
<text class="preview-title">图片列表 ({{ imageList.length }}/{{ MAX_IMAGES }})</text>
|
||
|
||
<!-- 多图预览列表 -->
|
||
<view class="multi-preview-container" v-if="imageList.length > 0">
|
||
<view
|
||
class="preview-item"
|
||
:class="{ 'preview-item-selected': currentImageIndex === index }"
|
||
v-for="(img, index) in imageList"
|
||
:key="index"
|
||
@click="handleSelectImage(index)"
|
||
>
|
||
<image :src="img.watermark" mode="aspectFill" class="preview-thumb"></image>
|
||
<view class="delete-btn" @click.stop="removeImage(index)">
|
||
<text class="delete-icon">×</text>
|
||
</view>
|
||
<text class="image-index">{{ index + 1 }}</text>
|
||
</view>
|
||
|
||
<!-- 添加更多图片按钮 -->
|
||
<view
|
||
class="add-more-btn"
|
||
v-if="imageList.length < MAX_IMAGES"
|
||
@click="handleAddMorePhoto"
|
||
>
|
||
<text class="add-icon">+</text>
|
||
<text class="add-text">添加</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 当前图片预览区域 -->
|
||
<view class="current-preview-box" v-if="imageSrc">
|
||
<text class="preview-title">当前图片预览</text>
|
||
<!-- 单图预览(显示当前选中的图片) -->
|
||
<view class="preview-img">
|
||
<image
|
||
:src="imageSrc"
|
||
@click="handlePreviewCurrentImage"
|
||
mode="aspectFit"
|
||
class="img"
|
||
></image>
|
||
</view>
|
||
|
||
<!-- 图片尺寸和文件大小信息 -->
|
||
<view
|
||
class="size-info"
|
||
v-if="imageSizeInfo.original.width > 0 && false"
|
||
>
|
||
<text class="size-text">
|
||
原图尺寸: {{ imageSizeInfo.original.width }} x
|
||
{{ imageSizeInfo.original.height }}
|
||
</text>
|
||
<text class="size-text" v-if="imageSizeInfo.original.fileSize">
|
||
原图大小: {{ imageSizeInfo.original.fileSize.sizeKB }} KB
|
||
</text>
|
||
<text class="size-text">
|
||
水印图片尺寸: {{ imageSizeInfo.watermark.width }} x
|
||
{{ imageSizeInfo.watermark.height }}
|
||
</text>
|
||
<text class="size-text" v-if="imageSizeInfo.watermark.fileSize">
|
||
水印图片大小: {{ imageSizeInfo.watermark.fileSize.sizeKB }} KB
|
||
</text>
|
||
</view>
|
||
</view>
|
||
<!-- 表单区域 -->
|
||
<view class="form-item">
|
||
<text class="label">位置:</text>
|
||
<input
|
||
v-model="locationInfo"
|
||
type="text"
|
||
class="input worker-input"
|
||
disabled
|
||
/>
|
||
</view>
|
||
<!-- 表单区域 -->
|
||
<view class="form-item">
|
||
<text class="label">请填写工作内容:</text>
|
||
<uni-combox
|
||
:candidates="candidates"
|
||
:clear-able="true"
|
||
placeholder="请填写工作内容"
|
||
@select="handleComboxSelect"
|
||
v-model="workContent"
|
||
></uni-combox>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="label">请选择部门:</text>
|
||
<picker :range="departments" @change="handleDeptChange">
|
||
<view class="picker">{{ departments[deptIndex] }}</view>
|
||
</picker>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="label">请填写施工人员:</text>
|
||
<view
|
||
v-for="(worker, index) in workers"
|
||
:key="index"
|
||
class="worker-row"
|
||
>
|
||
<input
|
||
type="text"
|
||
v-model="workers[index]"
|
||
class="input worker-input"
|
||
/>
|
||
<view class="btns">
|
||
<button class="btn_f" size="mini" @click="addWorker">+</button>
|
||
<button
|
||
v-if="workers.length > 0"
|
||
class="btn_f"
|
||
size="mini"
|
||
@click="removeWorker(index)"
|
||
>
|
||
-
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="label">请选择项目状态:</text>
|
||
<picker :range="statusList" @change="handleStatusChange">
|
||
<view class="picker">{{ statusList[statusIndex] }}</view>
|
||
</picker>
|
||
</view>
|
||
|
||
<!-- 底部操作按钮 -->
|
||
<view class="footer">
|
||
<view class="footer-row">
|
||
<button type="primary" class="btn-action btn-cancel" @click="handleRetakePhoto">
|
||
重新开始拍摄
|
||
</button>
|
||
<button type="primary" class="btn-action btn-continue" @click="handleAddMorePhoto">
|
||
继续拍照
|
||
</button>
|
||
</view>
|
||
<view class="footer-row">
|
||
<button type="primary" class="btn-action btn-delete" @click="handleDeleteCurrentImage">
|
||
删除图片
|
||
</button>
|
||
<button
|
||
type="primary"
|
||
class="btn-action btn-save"
|
||
@click="handleSaveImage"
|
||
:disabled="isSubmitting"
|
||
>
|
||
保存图片
|
||
</button>
|
||
<button
|
||
type="primary"
|
||
class="btn-action btn-submit"
|
||
@click="handleSaveAndSubmit"
|
||
:disabled="isSubmitting"
|
||
>
|
||
提交数据
|
||
</button>
|
||
</view>
|
||
|
||
<view class="footer-row" v-if="canShare">
|
||
<button
|
||
type="primary"
|
||
class="btn-action btn-share"
|
||
@click="handleShareImages"
|
||
:disabled="isSubmitting || imageList.length === 0"
|
||
>
|
||
分享图片
|
||
</button>
|
||
</view>
|
||
|
||
</view>
|
||
</view>
|
||
</uni-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref } from "vue";
|
||
// import eruda from "eruda";
|
||
import {
|
||
getLocation,
|
||
chooseImage,
|
||
addWatermark,
|
||
formatDate,
|
||
getCachedLogo,
|
||
saveImageToPhotosAlbum,
|
||
saveImagesToPhotosAlbum,
|
||
imageToBase64,
|
||
} from "../../common/utils";
|
||
import {
|
||
getLocationTranslate,
|
||
getLocationGeocoder,
|
||
} from "../../common/mapTranslateResult";
|
||
import { getConfig, addWatermarkRecord, addWatermarkRecordV2, addWatermarkRecordV3 } from "../../common/server";
|
||
import { batchUploadToCos } from "../../common/cosUpload";
|
||
import { onLoad } from "@dcloudio/uni-app";
|
||
// 简单初始化
|
||
// window.erudaInstance = eruda.init();
|
||
// ==================== 响应式数据 ====================
|
||
// 基础数据
|
||
const title = ref("随工水印相机");
|
||
const candidates = ref([]);
|
||
const workContent = ref(""); // 工作内容
|
||
const popup = ref(null);
|
||
|
||
// 位置信息
|
||
const locations = ref({
|
||
location: {
|
||
lat: null,
|
||
lng: null,
|
||
},
|
||
translate: {
|
||
lat: null,
|
||
lng: null,
|
||
locationInfo: null,
|
||
},
|
||
});
|
||
|
||
// 图片相关
|
||
const imageSrc = ref("");
|
||
const originalImageSrc = ref("");
|
||
// 多图支持
|
||
const imageList = ref([]); // 存储多张图片 [{original, watermark, sizeInfo}]
|
||
const MAX_IMAGES = 15; // 最大图片数量
|
||
const currentImageIndex = ref(0); // 当前选中的图片索引
|
||
const imageSizeInfo = ref({
|
||
original: {
|
||
width: 0,
|
||
height: 0,
|
||
fileSize: null,
|
||
},
|
||
watermark: {
|
||
width: 0,
|
||
height: 0,
|
||
fileSize: null,
|
||
},
|
||
});
|
||
|
||
// 表单数据
|
||
const departments = ref(["请选择"]);
|
||
const deptIndex = ref(0);
|
||
const workers = ref([""]);
|
||
const statusList = ref([]);
|
||
const statusIndex = ref(0);
|
||
var locationData = new Map();
|
||
// 其他信息
|
||
const locationInfo = ref("");
|
||
const currentTime = ref("");
|
||
const isSubmitting = ref(false); // 防抖状态
|
||
const canShare = ref(false); // 是否支持 Web Share API
|
||
let logo = ""; // 非响应式数据
|
||
|
||
// ==================== 事件处理函数 ====================
|
||
// 开始拍摄
|
||
const handleStartCapture = async () => {
|
||
uni.showLoading({
|
||
title: "加载中。。。",
|
||
});
|
||
|
||
try {
|
||
// 获取位置信息
|
||
const _locationData = await getLocation();
|
||
console.log("系统坐标", _locationData);
|
||
|
||
const location = _locationData.latitude + "," + _locationData.longitude;
|
||
console.log("经纬度", location);
|
||
|
||
const translate = await getLocationTranslate(location);
|
||
console.log("转换后", translate);
|
||
|
||
const t_location = translate.lat + "," + translate.lng;
|
||
const geocoderResult = await getLocationGeocoder(t_location);
|
||
console.log("地理编码结果", geocoderResult);
|
||
|
||
// 保存位置信息
|
||
locations.value.location.lat = _locationData.latitude;
|
||
locations.value.location.lng = _locationData.longitude;
|
||
locations.value.translate.lat = translate.lat;
|
||
locations.value.translate.lng = translate.lng;
|
||
locations.value.translate.locationInfo = geocoderResult || "未知位置";
|
||
locationInfo.value = geocoderResult || "未知位置";
|
||
currentTime.value = new Date();
|
||
|
||
// 选择图片(支持多选)
|
||
const image = await chooseImage(9); // 最多选择9张
|
||
console.log("图片", image);
|
||
|
||
// 处理多张图片
|
||
for (let i = 0; i < image.tempFilePaths.length && imageList.value.length < MAX_IMAGES; i++) {
|
||
const imgPath = image.tempFilePaths[i];
|
||
await addImageToList(imgPath);
|
||
}
|
||
|
||
// 设置当前显示的图片为第一张
|
||
if (imageList.value.length > 0) {
|
||
imageSrc.value = imageList.value[0].watermark;
|
||
originalImageSrc.value = imageList.value[0].original;
|
||
}
|
||
|
||
uni.hideLoading();
|
||
popup.value.open();
|
||
} catch (error) {
|
||
console.log("错误", error);
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: "获取位置失败",
|
||
icon: "error",
|
||
});
|
||
}
|
||
};
|
||
|
||
// 添加更多图片
|
||
const handleAddMorePhoto = async () => {
|
||
if (imageList.value.length >= MAX_IMAGES) {
|
||
uni.showToast({
|
||
title: `最多只能拍摄${MAX_IMAGES}张图片`,
|
||
icon: "none",
|
||
});
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const remainingSlots = MAX_IMAGES - imageList.value.length;
|
||
const image = await chooseImage(Math.min(remainingSlots, 9));
|
||
|
||
uni.showLoading({
|
||
title: "处理中...",
|
||
});
|
||
|
||
for (let i = 0; i < image.tempFilePaths.length && imageList.value.length < MAX_IMAGES; i++) {
|
||
const imgPath = image.tempFilePaths[i];
|
||
await addImageToList(imgPath);
|
||
}
|
||
|
||
uni.hideLoading();
|
||
} catch (error) {
|
||
console.log("添加图片失败:", error);
|
||
uni.hideLoading();
|
||
}
|
||
};
|
||
|
||
// 添加图片到列表(带水印处理)
|
||
const addImageToList = async (imgPath) => {
|
||
const watermarkInfo = {
|
||
time: formatDate(currentTime.value),
|
||
location: locationInfo.value,
|
||
longitude: locations.value.translate.lng,
|
||
latitude: locations.value.translate.lat,
|
||
department: departments.value[deptIndex.value],
|
||
workers: workers.value.filter((worker) => worker.trim() !== ""),
|
||
status: statusList.value[statusIndex.value],
|
||
remarks: workContent.value,
|
||
};
|
||
|
||
const watermarkResult = await addWatermark(imgPath, watermarkInfo, logo, 1);
|
||
|
||
imageList.value.push({
|
||
original: imgPath,
|
||
watermark: watermarkResult.filePath,
|
||
sizeInfo: {
|
||
original: watermarkResult.originalSize,
|
||
watermark: watermarkResult.watermarkSize,
|
||
},
|
||
});
|
||
};
|
||
|
||
// 删除图片(带二次确认弹窗)
|
||
const removeImage = (index) => {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '确定要删除该图片吗?',
|
||
confirmText: '确定',
|
||
cancelText: '取消',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
// 用户点击确定,执行删除
|
||
if (imageList.value.length > index) {
|
||
imageList.value.splice(index, 1);
|
||
// 更新当前显示的图片
|
||
if (imageList.value.length > 0) {
|
||
const newIndex = Math.min(index, imageList.value.length - 1);
|
||
currentImageIndex.value = newIndex;
|
||
imageSrc.value = imageList.value[newIndex].watermark;
|
||
originalImageSrc.value = imageList.value[newIndex].original;
|
||
} else {
|
||
currentImageIndex.value = 0;
|
||
imageSrc.value = "";
|
||
originalImageSrc.value = "";
|
||
}
|
||
}
|
||
}
|
||
// 用户点击取消,关闭弹窗,不做任何操作
|
||
}
|
||
});
|
||
};
|
||
|
||
// 删除当前选中的图片(底部按钮调用)
|
||
const handleDeleteCurrentImage = () => {
|
||
if (imageList.value.length === 0) {
|
||
uni.showToast({
|
||
title: '没有可删除的图片',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
removeImage(currentImageIndex.value);
|
||
};
|
||
|
||
// 选中指定索引的图片(点击缩略图)
|
||
const handleSelectImage = (index) => {
|
||
currentImageIndex.value = index;
|
||
imageSrc.value = imageList.value[index].watermark;
|
||
originalImageSrc.value = imageList.value[index].original;
|
||
};
|
||
|
||
// 预览指定索引的图片(长按或双击可用)
|
||
const handlePreviewImage = (index) => {
|
||
const urls = imageList.value.map((img) => img.watermark);
|
||
uni.previewImage({
|
||
urls: urls,
|
||
current: index,
|
||
});
|
||
};
|
||
|
||
// 预览当前图片
|
||
const handlePreviewCurrentImage = () => {
|
||
if (imageList.value.length > 0) {
|
||
const urls = imageList.value.map((img) => img.watermark);
|
||
uni.previewImage({
|
||
urls: urls,
|
||
});
|
||
} else if (imageSrc.value) {
|
||
uni.previewImage({
|
||
urls: [imageSrc.value],
|
||
});
|
||
}
|
||
};
|
||
|
||
// #ifdef H5
|
||
// 将图片 URL 转换为 File 对象
|
||
const urlToFile = async (url, filename) => {
|
||
const response = await fetch(url);
|
||
const blob = await response.blob();
|
||
return new File([blob], filename, { type: blob.type || 'image/jpeg' });
|
||
};
|
||
|
||
// 分享图片(H5 环境使用 Web Share API)
|
||
const handleShareImages = async () => {
|
||
if (imageList.value.length === 0) {
|
||
uni.showToast({
|
||
title: '没有可分享的图片',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (isSubmitting.value) {
|
||
return;
|
||
}
|
||
|
||
isSubmitting.value = true;
|
||
|
||
try {
|
||
uni.showLoading({
|
||
title: '准备分享...',
|
||
});
|
||
|
||
// 先刷新所有图片的水印,确保包含最新的工作内容等信息
|
||
await refreshAllWatermarks();
|
||
|
||
// 将所有水印图片转换为 File 对象
|
||
const files = [];
|
||
for (let i = 0; i < imageList.value.length; i++) {
|
||
const img = imageList.value[i];
|
||
const filename = `工作照片_${i + 1}.jpg`;
|
||
const file = await urlToFile(img.watermark, filename);
|
||
files.push(file);
|
||
}
|
||
|
||
// 构建分享文本
|
||
let shareText = '';
|
||
if (workContent.value) {
|
||
shareText += `工作内容:${workContent.value}\n`;
|
||
}
|
||
if (locationInfo.value) {
|
||
shareText += `位置:${locationInfo.value}\n`;
|
||
}
|
||
if (departments.value[deptIndex.value] && deptIndex.value > 0) {
|
||
shareText += `部门:${departments.value[deptIndex.value]}\n`;
|
||
}
|
||
const validWorkers = workers.value.filter(w => w.trim() !== '');
|
||
if (validWorkers.length > 0) {
|
||
shareText += `施工人员:${validWorkers.join('、')}\n`;
|
||
}
|
||
shareText += `共 ${files.length} 张照片`;
|
||
|
||
// 检查是否支持分享文件
|
||
const shareData = {
|
||
title: '工作照片分享',
|
||
text: shareText,
|
||
files: files
|
||
};
|
||
|
||
if (!navigator.canShare(shareData)) {
|
||
// 如果不支持分享文件,尝试只分享文本
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: '当前浏览器不支持分享图片',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
uni.hideLoading();
|
||
|
||
// 调用系统分享
|
||
await navigator.share(shareData);
|
||
|
||
uni.showToast({
|
||
title: '分享成功',
|
||
icon: 'success'
|
||
});
|
||
} catch (error) {
|
||
console.error('分享失败:', error);
|
||
uni.hideLoading();
|
||
|
||
// 用户取消分享不提示错误
|
||
if (error.name !== 'AbortError') {
|
||
uni.showToast({
|
||
title: '分享失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
} finally {
|
||
setTimeout(() => {
|
||
isSubmitting.value = false;
|
||
}, 500);
|
||
}
|
||
};
|
||
// #endif
|
||
|
||
// #ifdef APP-PLUS
|
||
// App 端分享图片(调用系统分享)
|
||
const handleShareImages = async () => {
|
||
if (imageList.value.length === 0) {
|
||
uni.showToast({
|
||
title: '没有可分享的图片',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (isSubmitting.value) {
|
||
return;
|
||
}
|
||
|
||
isSubmitting.value = true;
|
||
|
||
try {
|
||
uni.showLoading({
|
||
title: '准备分享...',
|
||
});
|
||
|
||
// 先刷新所有图片的水印,确保包含最新的工作内容等信息
|
||
await refreshAllWatermarks();
|
||
|
||
// 收集所有水印图片路径
|
||
const imagePaths = imageList.value.map(img => img.watermark);
|
||
|
||
// 构建分享文本
|
||
let shareText = '';
|
||
if (workContent.value) {
|
||
shareText += `工作内容:${workContent.value}\n`;
|
||
}
|
||
if (locationInfo.value) {
|
||
shareText += `位置:${locationInfo.value}\n`;
|
||
}
|
||
if (departments.value[deptIndex.value] && deptIndex.value > 0) {
|
||
shareText += `部门:${departments.value[deptIndex.value]}\n`;
|
||
}
|
||
const validWorkers = workers.value.filter(w => w.trim() !== '');
|
||
if (validWorkers.length > 0) {
|
||
shareText += `施工人员:${validWorkers.join('、')}\n`;
|
||
}
|
||
shareText += `共 ${imagePaths.length} 张照片`;
|
||
|
||
uni.hideLoading();
|
||
|
||
// 使用 plus.share 调用系统分享
|
||
plus.share.sendWithSystem({
|
||
type: 'image',
|
||
pictures: imagePaths,
|
||
content: shareText
|
||
}, () => {
|
||
console.log('分享成功');
|
||
uni.showToast({
|
||
title: '分享成功',
|
||
icon: 'success'
|
||
});
|
||
}, (err) => {
|
||
console.error('分享失败:', err);
|
||
// 用户取消分享不提示错误
|
||
if (err.code !== -2) {
|
||
uni.showToast({
|
||
title: '分享失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('分享失败:', error);
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: '分享失败',
|
||
icon: 'none'
|
||
});
|
||
} finally {
|
||
setTimeout(() => {
|
||
isSubmitting.value = false;
|
||
}, 500);
|
||
}
|
||
};
|
||
// #endif
|
||
|
||
// 表单事件处理
|
||
const handleComboxSelect = async (value) => {
|
||
console.log("选择的工作内容:", value);
|
||
if (locationData[value] != null) {
|
||
var tempData = locationData[value];
|
||
|
||
// 设置部门
|
||
if (tempData.dept && departments.value.includes(tempData.dept)) {
|
||
deptIndex.value = departments.value.indexOf(tempData.dept);
|
||
}
|
||
|
||
// 设置项目状态
|
||
if (tempData.status && statusList.value.includes(tempData.status)) {
|
||
statusIndex.value = statusList.value.indexOf(tempData.status);
|
||
}
|
||
|
||
// 设置施工人员
|
||
if (tempData.workers && Array.isArray(tempData.workers)) {
|
||
workers.value.splice(0, workers.value.length);
|
||
workers.value.push(...tempData.workers);
|
||
}
|
||
await addWatermarkToImage(value);
|
||
console.log("加载的历史数据:", tempData);
|
||
}
|
||
};
|
||
|
||
const handleDeptChange = (e) => {
|
||
deptIndex.value = e.detail.value;
|
||
};
|
||
|
||
const handleStatusChange = (e) => {
|
||
statusIndex.value = e.detail.value;
|
||
};
|
||
|
||
// 施工人员管理
|
||
const addWorker = () => {
|
||
workers.value.push("");
|
||
};
|
||
|
||
const removeWorker = (index) => {
|
||
if (workers.value.length > 1) {
|
||
workers.value.splice(index, 1);
|
||
}
|
||
};
|
||
|
||
// ==================== 业务逻辑函数 ====================
|
||
// 添加水印
|
||
const addWatermarkToImage = async (work = "") => {
|
||
try {
|
||
const watermarkInfo = {
|
||
time: formatDate(currentTime.value),
|
||
location: locationInfo.value,
|
||
longitude: locations.value.translate.lng,
|
||
latitude: locations.value.translate.lat,
|
||
department: departments.value[deptIndex.value],
|
||
workers: workers.value.filter((worker) => worker.trim() !== ""),
|
||
status: statusList.value[statusIndex.value],
|
||
remarks: workContent.value,
|
||
};
|
||
if (workContent.value == "" && work != "") {
|
||
watermarkInfo.remarks = work;
|
||
}
|
||
console.log("水印信息:", watermarkInfo);
|
||
console.log("原图路径:", originalImageSrc.value);
|
||
|
||
const watermarkResult = await addWatermark(
|
||
originalImageSrc.value,
|
||
watermarkInfo,
|
||
logo,
|
||
1
|
||
);
|
||
|
||
console.log("水印结果:", watermarkResult);
|
||
imageSrc.value = watermarkResult.filePath;
|
||
imageSizeInfo.value = {
|
||
original: watermarkResult.originalSize,
|
||
watermark: watermarkResult.watermarkSize,
|
||
};
|
||
} catch (error) {
|
||
console.error("添加水印失败:", error);
|
||
uni.showToast({
|
||
title: "添加水印失败",
|
||
icon: "error",
|
||
});
|
||
}
|
||
};
|
||
|
||
// 重新拍照
|
||
const handleRetakePhoto = () => {
|
||
popup.value.close();
|
||
resetFormData();
|
||
};
|
||
|
||
// 保存图片(仅生成水印并保存到相册)
|
||
const handleSaveImage = async () => {
|
||
// 防抖检查
|
||
if (isSubmitting.value) {
|
||
return;
|
||
}
|
||
|
||
// 基本验证
|
||
if (!validateFormData(false)) {
|
||
return;
|
||
}
|
||
|
||
isSubmitting.value = true;
|
||
|
||
try {
|
||
uni.showLoading({
|
||
title: "提交数据中...",
|
||
});
|
||
|
||
// 先添加水印
|
||
await addWatermarkToImage();
|
||
|
||
// 保存水印图片到相册
|
||
if (imageSrc.value) {
|
||
await saveImageToPhotosAlbum(imageSrc.value);
|
||
}
|
||
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: "图片保存成功",
|
||
icon: "success",
|
||
});
|
||
} catch (error) {
|
||
console.error("保存图片失败:", error);
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: "保存图片失败",
|
||
icon: "error",
|
||
});
|
||
} finally {
|
||
// 延迟重置状态,防止快速重复点击
|
||
setTimeout(() => {
|
||
isSubmitting.value = false;
|
||
}, 1000);
|
||
}
|
||
};
|
||
|
||
const handleRetakeCancel = async () => {
|
||
workers.value.splice(0, workers.value.length);
|
||
workers.value.push("");
|
||
statusIndex.value = 0;
|
||
deptIndex.value = 0;
|
||
workContent.value = "";
|
||
imageList.value = []; // 清空图片列表
|
||
loadCandidates();
|
||
await refreshAllWatermarks();
|
||
};
|
||
|
||
// 刷新所有图片的水印
|
||
const refreshAllWatermarks = async () => {
|
||
if (imageList.value.length === 0) return;
|
||
|
||
uni.showLoading({ title: "处理中..." });
|
||
|
||
const watermarkInfo = {
|
||
time: formatDate(currentTime.value),
|
||
location: locationInfo.value,
|
||
longitude: locations.value.translate.lng,
|
||
latitude: locations.value.translate.lat,
|
||
department: departments.value[deptIndex.value],
|
||
workers: workers.value.filter((worker) => worker.trim() !== ""),
|
||
status: statusList.value[statusIndex.value],
|
||
remarks: workContent.value,
|
||
};
|
||
|
||
for (let i = 0; i < imageList.value.length; i++) {
|
||
const img = imageList.value[i];
|
||
const watermarkResult = await addWatermark(img.original, watermarkInfo, logo, 1);
|
||
imageList.value[i].watermark = watermarkResult.filePath;
|
||
imageList.value[i].sizeInfo = {
|
||
original: watermarkResult.originalSize,
|
||
watermark: watermarkResult.watermarkSize,
|
||
};
|
||
}
|
||
|
||
// 更新当前显示的图片
|
||
if (imageList.value.length > 0) {
|
||
imageSrc.value = imageList.value[0].watermark;
|
||
}
|
||
|
||
uni.hideLoading();
|
||
};
|
||
|
||
// 回退到 V2 Base64 上传(当 COS 上传失败时使用)
|
||
const fallbackToV2Upload = async (_remarks, validWorkers) => {
|
||
try {
|
||
uni.showLoading({
|
||
title: "转换图片中...",
|
||
});
|
||
|
||
// 转换所有图片为 Base64
|
||
const imagesBase64 = [];
|
||
for (const img of imageList.value) {
|
||
const base64 = await imageToBase64(img.watermark);
|
||
imagesBase64.push(base64);
|
||
}
|
||
|
||
console.log(`回退到 V2 上传,共${imagesBase64.length}张图片`);
|
||
|
||
uni.showLoading({
|
||
title: "提交数据中...",
|
||
});
|
||
|
||
const camRecordWorkDto = {
|
||
// 部门名称
|
||
DeptName: departments.value[deptIndex.value],
|
||
// 图片地址(第一张作为封面,兼容旧版)
|
||
Image: imagesBase64[0],
|
||
// 多图支持
|
||
Images: imagesBase64,
|
||
// 工作记录时间
|
||
RecordTime: formatDate(currentTime.value),
|
||
// 经度
|
||
Longitude: locations.value.translate.lng,
|
||
// 纬度
|
||
Latitude: locations.value.translate.lat,
|
||
// 工作地点
|
||
Address: locationInfo.value,
|
||
// 工作内容
|
||
Content: workContent.value,
|
||
// 状态
|
||
StatusName: statusList.value[statusIndex.value],
|
||
// 备注
|
||
Remarks: _remarks,
|
||
// 工作人员列表
|
||
Workers: validWorkers,
|
||
};
|
||
|
||
// 使用 v2 API 支持多图
|
||
var res = await addWatermarkRecordV2(camRecordWorkDto);
|
||
console.log("V2 API 响应:", res);
|
||
|
||
if (res.code != 200) {
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: res.msg || "保存失败",
|
||
icon: "error",
|
||
});
|
||
return;
|
||
}
|
||
|
||
uni.hideLoading();
|
||
|
||
uni.showToast({
|
||
title: "提交成功",
|
||
icon: "success",
|
||
});
|
||
|
||
// 保存所有水印图片到相册
|
||
try {
|
||
if (imageList.value.length > 0) {
|
||
const watermarkPaths = imageList.value.map(img => img.watermark);
|
||
await saveImagesToPhotosAlbum(watermarkPaths);
|
||
}
|
||
} catch (saveError) {
|
||
console.error("保存图片到相册失败:", saveError);
|
||
}
|
||
|
||
handleRetakePhoto();
|
||
} catch (error) {
|
||
console.error("V2 上传失败:", error);
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: "保存失败",
|
||
icon: "error",
|
||
});
|
||
} finally {
|
||
setTimeout(() => {
|
||
isSubmitting.value = false;
|
||
}, 1000);
|
||
}
|
||
};
|
||
|
||
// 保存并提交
|
||
const handleSaveAndSubmit = async () => {
|
||
// 防抖检查
|
||
if (isSubmitting.value) {
|
||
return;
|
||
}
|
||
|
||
// 参数验证
|
||
if (!validateFormData(true)) {
|
||
return;
|
||
}
|
||
|
||
// 检查是否有图片
|
||
if (imageList.value.length === 0) {
|
||
uni.showToast({
|
||
title: "请先拍摄图片",
|
||
icon: "error",
|
||
});
|
||
return;
|
||
}
|
||
|
||
isSubmitting.value = true;
|
||
|
||
try {
|
||
uni.showLoading({
|
||
title: "保存中...",
|
||
});
|
||
|
||
// 刷新所有图片的水印(确保最新信息)
|
||
await refreshAllWatermarks();
|
||
|
||
// 保存本地历史数据
|
||
var saveData = {
|
||
workContent: workContent.value,
|
||
workers: workers.value.filter((worker) => worker.trim() !== ""),
|
||
status: statusList.value[statusIndex.value],
|
||
dept: departments.value[deptIndex.value],
|
||
date: new Date().toISOString(),
|
||
};
|
||
console.log("locationData", locationData);
|
||
|
||
locationData[workContent.value] = saveData;
|
||
uni.setStorageSync("locationData", locationData);
|
||
|
||
var _remarks = JSON.stringify(locations.value);
|
||
const validWorkers = workers.value.filter((worker) => worker.trim() !== "");
|
||
|
||
// 收集所有水印图片路径
|
||
const watermarkPaths = imageList.value.map(img => img.watermark);
|
||
console.log(`共${watermarkPaths.length}张图片,开始上传到 COS...`);
|
||
|
||
// 使用 COS 直传
|
||
uni.showLoading({
|
||
title: "上传图片中...",
|
||
});
|
||
|
||
let imageUrls;
|
||
try {
|
||
imageUrls = await batchUploadToCos(
|
||
watermarkPaths,
|
||
{
|
||
recordTime: currentTime.value,
|
||
deptName: departments.value[deptIndex.value],
|
||
content: workContent.value,
|
||
workers: validWorkers
|
||
},
|
||
(stage, completed, total) => {
|
||
if (stage === "getting_urls") {
|
||
uni.showLoading({ title: "获取上传地址..." });
|
||
} else if (stage === "uploading") {
|
||
uni.showLoading({ title: `上传中 ${completed}/${total}...` });
|
||
}
|
||
}
|
||
);
|
||
} catch (cosError) {
|
||
console.error("COS 上传失败,回退到 Base64 上传:", cosError);
|
||
// COS 上传失败,回退到 V2 Base64 上传
|
||
uni.showLoading({ title: "切换上传方式..." });
|
||
await fallbackToV2Upload(_remarks, validWorkers);
|
||
return;
|
||
}
|
||
|
||
console.log("COS 上传成功,图片 URL:", imageUrls);
|
||
|
||
// 使用 V3 API 保存记录
|
||
uni.showLoading({
|
||
title: "保存记录中...",
|
||
});
|
||
|
||
const camRecordWorkV3Dto = {
|
||
// 部门名称
|
||
DeptName: departments.value[deptIndex.value],
|
||
// 工作记录时间
|
||
RecordTime: formatDate(currentTime.value),
|
||
// 经度
|
||
Longitude: String(locations.value.translate.lng),
|
||
// 纬度
|
||
Latitude: String(locations.value.translate.lat),
|
||
// 工作地点
|
||
Address: locationInfo.value,
|
||
// 工作内容
|
||
Content: workContent.value,
|
||
// 状态
|
||
StatusName: statusList.value[statusIndex.value],
|
||
// 备注
|
||
Remarks: _remarks,
|
||
// 工作人员列表
|
||
Workers: validWorkers,
|
||
// COS 图片 URL 列表
|
||
ImageUrls: imageUrls,
|
||
};
|
||
|
||
var res = await addWatermarkRecordV3(camRecordWorkV3Dto);
|
||
console.log("V3 API 响应:", res);
|
||
|
||
if (res.code != 200) {
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: res.msg || "保存失败",
|
||
icon: "error",
|
||
});
|
||
return;
|
||
}
|
||
|
||
uni.hideLoading();
|
||
|
||
uni.showToast({
|
||
title: "提交成功",
|
||
icon: "success",
|
||
});
|
||
} catch (error) {
|
||
console.error("保存失败:", error);
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: "保存失败",
|
||
icon: "error",
|
||
});
|
||
} finally {
|
||
// 延迟重置状态,防止快速重复点击
|
||
setTimeout(() => {
|
||
isSubmitting.value = false;
|
||
}, 1000);
|
||
}
|
||
|
||
try {
|
||
// 保存所有水印图片到相册
|
||
if (imageList.value.length > 0) {
|
||
const watermarkPaths = imageList.value.map(img => img.watermark);
|
||
const saveResult = await saveImagesToPhotosAlbum(watermarkPaths);
|
||
console.log(`保存到相册完成: 成功${saveResult.success}张, 失败${saveResult.failed}张`);
|
||
}
|
||
} catch (error) {
|
||
console.error("保存图片失败", error);
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: "保存图片失败",
|
||
icon: "error",
|
||
});
|
||
}
|
||
handleRetakePhoto();
|
||
};
|
||
|
||
// ==================== 工具函数 ====================
|
||
// 验证表单数据
|
||
const validateFormData = (isSubmit = false) => {
|
||
// 基本验证 - 位置信息
|
||
if (!locationInfo.value || locationInfo.value.trim() === "") {
|
||
uni.showToast({
|
||
title: "位置信息不能为空",
|
||
icon: "error",
|
||
});
|
||
return false;
|
||
}
|
||
|
||
// 如果是提交操作,需要更严格的验证
|
||
if (isSubmit) {
|
||
if (!workContent.value || workContent.value.trim() === "") {
|
||
uni.showToast({
|
||
title: "请填写工作内容",
|
||
icon: "error",
|
||
});
|
||
return false;
|
||
}
|
||
|
||
if (deptIndex.value === 0) {
|
||
uni.showToast({
|
||
title: "请选择部门",
|
||
icon: "error",
|
||
});
|
||
return false;
|
||
}
|
||
|
||
const validWorkers = workers.value.filter((worker) => worker.trim() !== "");
|
||
if (validWorkers.length === 0) {
|
||
uni.showToast({
|
||
title: "请填写施工人员",
|
||
icon: "error",
|
||
});
|
||
return false;
|
||
}
|
||
|
||
// if (statusIndex.value === 0) {
|
||
// uni.showToast({
|
||
// title: "请选择项目状态",
|
||
// icon: "error",
|
||
// });
|
||
// return false;
|
||
// }
|
||
}
|
||
|
||
return true;
|
||
};
|
||
|
||
// 重置表单数据
|
||
const resetFormData = () => {
|
||
imageSrc.value = "";
|
||
originalImageSrc.value = "";
|
||
imageList.value = []; // 清空图片列表
|
||
locationInfo.value = "";
|
||
currentTime.value = "";
|
||
imageSizeInfo.value = {
|
||
original: {
|
||
width: 0,
|
||
height: 0,
|
||
fileSize: null,
|
||
},
|
||
watermark: {
|
||
width: 0,
|
||
height: 0,
|
||
fileSize: null,
|
||
},
|
||
};
|
||
workers.value.splice(0, workers.value.length);
|
||
workers.value.push("");
|
||
statusIndex.value = 0;
|
||
deptIndex.value = 0;
|
||
workContent.value = "";
|
||
loadCandidates();
|
||
};
|
||
|
||
const loadCandidates = () => {
|
||
candidates.value.splice(0, candidates.value.length);
|
||
|
||
// 将对象转换为数组并按时间倒序排序
|
||
const sortedEntries = Object.entries(locationData)
|
||
.filter(([key, data]) => data && data.date) // 过滤掉没有时间字段的数据
|
||
.sort((a, b) => {
|
||
const dateA = new Date(a[1].date);
|
||
const dateB = new Date(b[1].date);
|
||
return dateB - dateA; // 倒序排列(最新的在前)
|
||
});
|
||
|
||
// 提取排序后的键名
|
||
sortedEntries.forEach(([key, data]) => {
|
||
console.log(`工作内容: ${key}, 时间: ${data.date}`);
|
||
candidates.value.push(key);
|
||
});
|
||
};
|
||
// ==================== 生命周期 ====================
|
||
onLoad(async () => {
|
||
uni.showLoading({
|
||
title: "loading...",
|
||
});
|
||
|
||
// #ifdef H5
|
||
// 检测 Web Share API 支持
|
||
if (navigator.share && navigator.canShare) {
|
||
canShare.value = true;
|
||
}
|
||
// #endif
|
||
|
||
// #ifdef APP-PLUS
|
||
// App 端始终支持系统分享
|
||
canShare.value = true;
|
||
// #endif
|
||
|
||
try {
|
||
const config = await getConfig();
|
||
console.log("配置", config);
|
||
|
||
logo = config.logo;
|
||
departments.value.push(...config.deptList);
|
||
statusList.value.push(...config.construction);
|
||
|
||
// 缓存logo
|
||
await getCachedLogo(logo);
|
||
|
||
var _locationData = uni.getStorageSync("locationData");
|
||
if (_locationData != null && _locationData != "") {
|
||
locationData = _locationData;
|
||
loadCandidates();
|
||
}
|
||
} catch (error) {
|
||
console.error("初始化失败:", error);
|
||
} finally {
|
||
uni.hideLoading();
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style>
|
||
.logo {
|
||
height: 200px;
|
||
width: 200px;
|
||
margin: 0 auto;
|
||
position: absolute;
|
||
top: 25%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
|
||
.btn {
|
||
width: 100px;
|
||
margin: 0 auto;
|
||
position: absolute;
|
||
bottom: 22%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
|
||
.title {
|
||
margin: 0 auto;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #000;
|
||
text-align: center;
|
||
position: absolute;
|
||
top: 35%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
|
||
.container {
|
||
padding: 20rpx;
|
||
background: #fff;
|
||
height: 100vh;
|
||
overflow: auto;
|
||
}
|
||
|
||
.preview-box {
|
||
background: #fff;
|
||
padding: 20rpx;
|
||
margin-bottom: 20rpx;
|
||
text-align: center;
|
||
border-bottom: 1rpx solid #eee;
|
||
}
|
||
|
||
/* 当前图片预览区域 */
|
||
.current-preview-box {
|
||
background: #fff;
|
||
padding: 20rpx;
|
||
margin-bottom: 20rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.preview-title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* 多图预览容器 */
|
||
.multi-preview-container {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16rpx;
|
||
margin: 20rpx 0;
|
||
padding: 16rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 12rpx;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.preview-item {
|
||
position: relative;
|
||
width: 140rpx;
|
||
height: 140rpx;
|
||
border-radius: 8rpx;
|
||
overflow: hidden;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
|
||
border: 4rpx solid transparent;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* 选中状态 */
|
||
.preview-item-selected {
|
||
border: 4rpx solid #007aff;
|
||
box-shadow: 0 4rpx 12rpx rgba(0, 122, 255, 0.4);
|
||
}
|
||
|
||
.preview-thumb {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.delete-btn {
|
||
position: absolute;
|
||
top: 4rpx;
|
||
right: 4rpx;
|
||
width: 36rpx;
|
||
height: 36rpx;
|
||
background: rgba(255, 0, 0, 0.8);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.delete-icon {
|
||
color: #fff;
|
||
font-size: 24rpx;
|
||
font-weight: bold;
|
||
line-height: 1;
|
||
}
|
||
|
||
.image-index {
|
||
position: absolute;
|
||
bottom: 4rpx;
|
||
left: 4rpx;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
color: #fff;
|
||
font-size: 20rpx;
|
||
padding: 2rpx 8rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.add-more-btn {
|
||
width: 140rpx;
|
||
height: 140rpx;
|
||
border: 2rpx dashed #007aff;
|
||
border-radius: 8rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
background: #f0f8ff;
|
||
}
|
||
|
||
.add-icon {
|
||
font-size: 48rpx;
|
||
color: #007aff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.add-text {
|
||
font-size: 20rpx;
|
||
color: #007aff;
|
||
margin-top: 4rpx;
|
||
}
|
||
|
||
.preview-img {
|
||
margin: 20rpx 0;
|
||
width: 100%;
|
||
height: 400rpx;
|
||
background: #ddd;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
text-align: center;
|
||
}
|
||
|
||
.img {
|
||
max-width: 100%;
|
||
max-height: 400rpx;
|
||
}
|
||
|
||
.preview-info {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
text-align: left;
|
||
margin-top: 10rpx;
|
||
}
|
||
|
||
.form-item {
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.label {
|
||
font-size: 28rpx;
|
||
margin-bottom: 10rpx;
|
||
display: block;
|
||
}
|
||
|
||
.input {
|
||
border: 1px solid #ddd;
|
||
border-radius: 8rpx;
|
||
padding: 10rpx;
|
||
background: #fff;
|
||
}
|
||
|
||
.picker {
|
||
border: 1px solid #ddd;
|
||
border-radius: 8rpx;
|
||
padding: 10rpx;
|
||
background: #fff;
|
||
}
|
||
|
||
.worker-row {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.worker-input {
|
||
flex: 1;
|
||
}
|
||
|
||
.btns {
|
||
display: flex;
|
||
margin-left: 10rpx;
|
||
}
|
||
|
||
.btn_f {
|
||
margin-left: 5rpx;
|
||
}
|
||
|
||
.footer {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20rpx;
|
||
margin-top: 40rpx;
|
||
padding-bottom: 40rpx;
|
||
}
|
||
|
||
.footer-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.btn-action {
|
||
flex: 1;
|
||
font-size: 26rpx;
|
||
padding: 16rpx 0;
|
||
margin: 0;
|
||
}
|
||
|
||
.btn-cancel {
|
||
background: #007aff;
|
||
color: #fff;
|
||
}
|
||
|
||
.btn-continue {
|
||
background: #17a2b8;
|
||
color: #fff;
|
||
}
|
||
|
||
.btn-delete {
|
||
background: #dc3545;
|
||
color: #fff;
|
||
}
|
||
|
||
.btn-save {
|
||
background: #28a745;
|
||
color: #fff;
|
||
}
|
||
|
||
.btn-submit {
|
||
background: #007aff;
|
||
color: #fff;
|
||
}
|
||
|
||
.btn-share {
|
||
background: #ff9500;
|
||
color: #fff;
|
||
}
|
||
|
||
.btn-save:disabled,
|
||
.btn-submit:disabled,
|
||
.btn-share:disabled,
|
||
.btn-action:disabled {
|
||
background: #ccc !important;
|
||
color: #999 !important;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.watermark-btn {
|
||
margin-top: 20rpx;
|
||
background: #28a745;
|
||
color: #fff;
|
||
font-size: 28rpx;
|
||
padding: 10rpx 30rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.size-info {
|
||
margin-top: 20rpx;
|
||
padding: 20rpx;
|
||
background: #f8f9fa;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.size-text {
|
||
display: block;
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
margin-bottom: 10rpx;
|
||
text-align: center;
|
||
}
|
||
</style> |