WorkCameraf/pages/index/index.vue
2025-08-20 15:43:24 +08:00

847 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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">图片预览</text>
<view class="preview-img">
<image
:src="imageSrc"
@click="handlePreviewImage"
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">
<button type="primary" class="btn-cancel" @click="handleRetakePhoto">
重拍
</button>
<button type="primary" class="btn-cancel" @click="handleRetakeCancel">
清空
</button>
<button
type="primary"
class="btn-save"
@click="handleSaveImage"
:disabled="isSubmitting"
>
保存图片
</button>
<button
type="primary"
class="btn-submit"
@click="handleSaveAndSubmit"
:disabled="isSubmitting"
>
提交数据
</button>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref } from "vue";
// import eruda from "eruda";
import {
getLocation,
chooseImage,
addWatermark,
formatDate,
getCachedLogo,
saveImageToPhotosAlbum,
imageToBase64,
} from "../../common/utils";
import {
getLocationTranslate,
getLocationGeocoder,
} from "../../common/mapTranslateResult";
import { getConfig, addWatermarkRecord } from "../../common/server";
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 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); // 防抖状态
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();
console.log("图片", image);
originalImageSrc.value = image.tempFilePaths[0];
imageSrc.value = image.tempFilePaths[0];
await addWatermarkToImage();
uni.hideLoading();
popup.value.open();
} catch (error) {
console.log("错误", error);
uni.hideLoading();
}
};
// 预览图片
const handlePreviewImage = () => {
uni.previewImage({
urls: [imageSrc.value],
});
};
// 表单事件处理
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 = "";
loadCandidates();
await addWatermarkToImage();
};
// 保存并提交
const handleSaveAndSubmit = async () => {
// 防抖检查
if (isSubmitting.value) {
return;
}
// 参数验证
if (!validateFormData(true)) {
return;
}
isSubmitting.value = true;
try {
uni.showLoading({
title: "保存中...",
});
// 先添加水印
await addWatermarkToImage();
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(), // 使用ISO格式确保时间格式一致
};
console.log("locationData", locationData);
locationData[workContent.value] = saveData;
uni.setStorageSync("locationData", locationData);
var fromData = {
locations: locations.value,
workContent: workContent.value,
workers: workers.value,
status: statusList.value[statusIndex.value],
dept: departments.value[deptIndex.value],
};
var _remarks = JSON.stringify(locations.value);
var imageBase64 = await imageToBase64(imageSrc.value);
console.log(fromData);
const camRecordWorkDto = {
// 部门名称
DeptName: departments.value[deptIndex.value],
// 图片地址
Image: imageBase64,
// 工作记录时间
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: workers.value,
};
var res = await addWatermarkRecord(camRecordWorkDto);
console.log(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 (imageSrc.value) {
await saveImageToPhotosAlbum(imageSrc.value);
}
} 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 = "";
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...",
});
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;
}
.preview-title {
font-size: 32rpx;
font-weight: bold;
}
.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;
justify-content: space-between;
margin-top: 40rpx;
}
.btn-cancel {
background: #007aff;
color: #fff;
}
.btn-save {
background: #28a745;
color: #fff;
}
.btn-submit {
background: #007aff;
color: #fff;
}
.btn-save:disabled,
.btn-submit: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>