diff --git a/server/Zr.Admin.NET/ZR.Admin.WebApi/Controllers/Business/CamWorkrecordController.cs b/server/Zr.Admin.NET/ZR.Admin.WebApi/Controllers/Business/CamWorkrecordController.cs index 862d62d..d720f71 100644 --- a/server/Zr.Admin.NET/ZR.Admin.WebApi/Controllers/Business/CamWorkrecordController.cs +++ b/server/Zr.Admin.NET/ZR.Admin.WebApi/Controllers/Business/CamWorkrecordController.cs @@ -34,14 +34,16 @@ namespace ZR.Admin.WebApi.Controllers.Business private OptionsSetting OptionsSetting; private IWebHostEnvironment WebHostEnvironment; private readonly ICamWorkerService _CamWorkerService; + private readonly ICamWorkrecordImageService _CamWorkrecordImageService; public CamWorkrecordController(ICamWorkrecordService CamWorkrecordService, IOptions options, IWebHostEnvironment webHostEnvironment, - ICamWorkerService camWorkerService) + ICamWorkerService camWorkerService, ICamWorkrecordImageService camWorkrecordImageService) { _CamWorkrecordService = CamWorkrecordService; OptionsSetting = options.Value; WebHostEnvironment = webHostEnvironment; _CamWorkerService = camWorkerService; + _CamWorkrecordImageService = camWorkrecordImageService; } /// @@ -54,6 +56,37 @@ namespace ZR.Admin.WebApi.Controllers.Business public IActionResult QueryCamWorkrecord([FromQuery] CamWorkrecordQueryDto parm) { var response = _CamWorkrecordService.GetList(parm); + + // 获取所有记录的图片 + var ids = response.Result.Select(x => x.Id).ToList(); + if (ids.Count > 0) + { + var allImages = _CamWorkrecordImageService.GetListByWorkrecordIds(ids); + var imagesByRecordId = allImages.GroupBy(x => x.WorkrecordId) + .ToDictionary(g => g.Key, g => g.OrderBy(x => x.SortOrder).ToList()); + + response.Result.ForEach(item => + { + if (imagesByRecordId.TryGetValue(item.Id, out var imgs)) + { + item.Images = imgs.Select(x => new CamWorkrecordImageDto + { + Id = x.Id, + WorkrecordId = x.WorkrecordId, + ImageUrl = x.ImageUrl, + SortOrder = x.SortOrder, + CreateTime = x.CreateTime + }).ToList(); + item.ImageCount = imgs.Count; + } + else + { + item.Images = new List(); + item.ImageCount = 0; + } + }); + } + return SUCCESS(response); } @@ -74,6 +107,18 @@ namespace ZR.Admin.WebApi.Controllers.Business { var names = _CamWorkerService.AsQueryable().Where(it => it.WorkrecordId == info.Id).Select(it => it.WorkerName).ToList(); info.Worker = names != null && names.Count > 0 ? string.Join(",", names) : ""; + + // 获取图片列表 + var imageRecords = _CamWorkrecordImageService.GetListByWorkrecordId(info.Id); + info.Images = imageRecords.Select(x => new CamWorkrecordImageDto + { + Id = x.Id, + WorkrecordId = x.WorkrecordId, + ImageUrl = x.ImageUrl, + SortOrder = x.SortOrder, + CreateTime = x.CreateTime + }).ToList(); + info.ImageCount = imageRecords.Count; } return SUCCESS(info); } @@ -141,9 +186,17 @@ namespace ZR.Admin.WebApi.Controllers.Business } var url = OptionsSetting.Upload.UploadUrl; string savePath = Path.Combine(WebHostEnvironment.WebRootPath); + + // 获取所有记录的图片 + var ids = list.Select(x => x.Id).ToList(); + var allImages = _CamWorkrecordImageService.GetListByWorkrecordIds(ids); + var imagesByRecordId = allImages.GroupBy(x => x.WorkrecordId) + .ToDictionary(g => g.Key, g => g.OrderBy(x => x.SortOrder).Select(x => x.ImageUrl).ToList()); + int i = 1; list.ForEach(it => { + // 处理主图(兼容旧数据) var temp = string.IsNullOrEmpty(it.ImageUrl) ? "" : it.ImageUrl; if (temp.IndexOf("http") > -1) { @@ -152,20 +205,26 @@ namespace ZR.Admin.WebApi.Controllers.Business } it.Index = i; it.ImageUrl = temp; - //it.Image = CompressImage(temp, 50); i++; }); ExcelHelper excelHelper = new ExcelHelper(WebHostEnvironment); ExcelPackage.License.SetNonCommercialPersonal("pnaa"); - var result = ExportWorkRecordExcel(list, "工作记录", 200, 70, 60); - //ExportExcelMini(list, "工作记录", "工作记录"); - //ExportExcelWithImagesInBatches(result.Item2, "工作记录", urlList, 10); - //loadExcel(); + var result = ExportWorkRecordExcel(list, imagesByRecordId, url, savePath, "工作记录", 100, 70, 60); return ExportExcel(result.Item2, result.Item1); } /// - public (string fileName, string fullPath) ExportWorkRecordExcel(List list, string fileName, int imageWidth = 100, int imageHeight = 60, int imageQuality = 50) + /// 导出工作记录Excel(支持多图) + /// + public (string fileName, string fullPath) ExportWorkRecordExcel( + List list, + Dictionary> imagesByRecordId, + string uploadUrl, + string savePath, + string fileName, + int imageWidth = 100, + int imageHeight = 60, + int imageQuality = 50) { IWebHostEnvironment webHostEnvironment = (IWebHostEnvironment)App.ServiceProvider.GetService(typeof(IWebHostEnvironment)); string sFileName = $"{fileName}_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx"; @@ -185,11 +244,8 @@ namespace ZR.Admin.WebApi.Controllers.Business { ws.Cells[1, i + 1].Value = headers[i]; ws.Cells[1, i + 1].Style.Font.Bold = true; - //ws.Column(i).Width = 120; } - //ws.Column(10).Width = 40; - int row = 2; foreach (var item in list) { @@ -205,26 +261,62 @@ namespace ZR.Admin.WebApi.Controllers.Business ws.Cells[row, 11].Value = item.CreateTime?.ToString("yyyy-MM-dd HH:mm:ss"); ws.Cells[row, 12].Value = item.UpdateTime?.ToString("yyyy-MM-dd HH:mm:ss"); - // 如果 ImageUrl 有值,则压缩后插入 Excel - if (!string.IsNullOrEmpty(item.ImageUrl) && IOFile.Exists(item.ImageUrl)) + // 获取该记录的所有图片 + var imageUrls = new List(); + if (imagesByRecordId.TryGetValue(item.Id, out var imgs) && imgs.Count > 0) { - using var image = SixLabors.ImageSharp.Image.Load(item.ImageUrl); - var encoder = new JpegEncoder { Quality = imageQuality }; - using var ms = new MemoryStream(); - image.Save(ms, encoder); - ms.Position = 0; - - var pic = ws.Drawings.AddPicture($"Pic_{row}_10", ms); - pic.SetPosition(row - 1, 5, 9, 5); // 第10列 - pic.SetSize(imageWidth, imageHeight); + imageUrls = imgs; } + else if (!string.IsNullOrEmpty(item.ImageUrl)) + { + // 兼容旧数据:使用主表的ImageUrl + imageUrls.Add(item.ImageUrl.Contains("http") ? item.ImageUrl : ""); + } + + // 插入多张图片(水平排列) + int picIndex = 0; + int totalImageWidth = 0; + foreach (var imgUrl in imageUrls) + { + var localPath = imgUrl; + if (imgUrl.Contains("http")) + { + localPath = savePath + imgUrl.Replace(uploadUrl, ""); + } + + if (!string.IsNullOrEmpty(localPath) && IOFile.Exists(localPath)) + { + try + { + using var image = SixLabors.ImageSharp.Image.Load(localPath); + var encoder = new JpegEncoder { Quality = imageQuality }; + using var ms = new MemoryStream(); + image.Save(ms, encoder); + ms.Position = 0; + + var pic = ws.Drawings.AddPicture($"Pic_{row}_{picIndex}", ms); + // 水平排列:每张图片偏移 imageWidth 像素 + int pixelOffset = picIndex * (imageWidth + 5); + pic.SetPosition(row - 1, 5, 9, 5 + pixelOffset); + pic.SetSize(imageWidth, imageHeight); + totalImageWidth = (picIndex + 1) * (imageWidth + 5); + picIndex++; + } + catch + { + // 忽略无法加载的图片 + } + } + } + ws.Row(row).Height = imageHeight; row++; } ws.Cells[ws.Dimension.Address].AutoFitColumns(); ws.Cells[ws.Dimension.Address].Style.VerticalAlignment = OfficeOpenXml.Style.ExcelVerticalAlignment.Center; - ws.Column(10).Width = 40; + // 设置施工图片列宽度(根据最大图片数量调整) + ws.Column(10).Width = 60; package.SaveAs(new FileInfo(fullPath)); return (sFileName, fullPath); diff --git a/server/Zr.Admin.NET/ZR.Admin.WebApi/Controllers/CommonController.cs b/server/Zr.Admin.NET/ZR.Admin.WebApi/Controllers/CommonController.cs index 111bc1c..0ac7e4f 100644 --- a/server/Zr.Admin.NET/ZR.Admin.WebApi/Controllers/CommonController.cs +++ b/server/Zr.Admin.NET/ZR.Admin.WebApi/Controllers/CommonController.cs @@ -47,6 +47,11 @@ namespace ZR.Admin.WebApi.Controllers /// 工作人员记录接口 /// private readonly ICamWorkerService _CamWorkerService; + + /// + /// 工作记录图片接口 + /// + private readonly ICamWorkrecordImageService _CamWorkrecordImageService; /// /// CommonController 构造函数 /// @@ -63,7 +68,8 @@ namespace ZR.Admin.WebApi.Controllers ISysDeptService deptService, ISysDictDataService sysDictDataService, ICamWorkrecordService _CamWorkrecordService, - ICamWorkerService camWorkerService) + ICamWorkerService camWorkerService, + ICamWorkrecordImageService camWorkrecordImageService) { WebHostEnvironment = webHostEnvironment; SysFileService = fileService; @@ -73,6 +79,7 @@ namespace ZR.Admin.WebApi.Controllers this.sysDictDataService = sysDictDataService; this._CamWorkrecordService = _CamWorkrecordService; _CamWorkerService = camWorkerService; + _CamWorkrecordImageService = camWorkrecordImageService; } /// @@ -443,6 +450,275 @@ namespace ZR.Admin.WebApi.Controllers return SUCCESS(1); } + /// + /// 添加工作记录(多图支持 v2) + /// + /// + [HttpPost] + [Route("/addworkrecordv2")] + [AllowAnonymous] + public async Task AddCamWorkRecordV2([FromBody] CamRecordWorkDto parm) + { + if (parm.Workers == null || parm.Workers.Count == 0) + { + return ToResponse(ResultCode.PARAM_ERROR, "请选择工作人员"); + } + + // 获取图片列表(优先使用 Images,兼容旧版 Image) + var imageList = new List(); + if (parm.Images != null && parm.Images.Count > 0) + { + imageList = parm.Images; + } + else if (!string.IsNullOrEmpty(parm.Image)) + { + imageList.Add(parm.Image); + } + + if (imageList.Count == 0) + { + return ToResponse(ResultCode.PARAM_ERROR, "请上传图片"); + } + + // 限制最多15张图片 + if (imageList.Count > 15) + { + return ToResponse(ResultCode.PARAM_ERROR, "最多支持15张图片"); + } + + var filePath = "/workfiles/" + parm.RecordTime?.ToString("yyyyMM/yyyyMMdd"); + string savePath = Path.Combine(WebHostEnvironment.WebRootPath); + var path = savePath + filePath; + var domainUrl = $"{OptionsSetting.Upload.UploadUrl}"; + + // 如果是编辑模式,先删除旧的图片 + if (parm.Id != null && parm.Id > 0) + { + var m = await _CamWorkrecordService.AsQueryable().Where(it => it.Id == parm.Id).FirstAsync(); + var m_workers = await _CamWorkerService.AsQueryable().Where(it => it.WorkrecordId == parm.Id).ToListAsync(); + if (m == null) + { + return ToResponse(ResultCode.PARAM_ERROR, "未找到该记录"); + } + + // 删除旧的图片记录 + var oldImages = _CamWorkrecordImageService.GetListByWorkrecordId((int)parm.Id); + foreach (var oldImg in oldImages) + { + await DeleteImageFiles(oldImg.ImageUrl, m.RecordTime, m_workers, m.Content, m.DeptName); + } + _CamWorkrecordImageService.DeleteByWorkrecordId((int)parm.Id); + + // 删除旧的主图 + if (!string.IsNullOrEmpty(m.ImageUrl) && m.ImageUrl.IndexOf("http") != -1) + { + await DeleteImageFiles(m.ImageUrl, m.RecordTime, m_workers, m.Content, m.DeptName); + } + + // 删除施工人员数据 + await _CamWorkerService.DeleteAsync(it => it.WorkrecordId == parm.Id); + } + + // 保存图片列表 + var savedImageUrls = new List(); + int sortOrder = 1; + + foreach (var imageBase64 in imageList) + { + var imageprx = ImageConverter.GetFileExtensionFromBase64(imageBase64); + var images = ImageConverter.Base64ToImageBytes(imageBase64); + if (images.Length == 0) + { + continue; + } + + int MaxSize = 300 * 1024; // 300KB + if (images.Length > MaxSize) + { + images = CompressImage(images, 50); + imageprx = ".jpg"; + } + + var imageName = ImageConverter.GenerateImageFileName(imageprx); + var participantsUrl = $"{path}/参与人员/"; + var photosDay = $"{path}/当日照片/"; + var jobContent = $"{path}/工作内容/"; + var department = $"{path}/部门/"; + + // 添加当日照片 + if (!Directory.Exists(photosDay)) + { + Directory.CreateDirectory(photosDay); + } + var photosDayFileName = $"{photosDay}/{imageName}"; + using (var stream = new FileStream(photosDayFileName, FileMode.Create)) + { + await stream.WriteAsync(images, 0, images.Length); + } + + // 添加当日根据【人名】分类的照片 + foreach (var work in parm.Workers) + { + var participantsUrlFIleName = $"{participantsUrl}/{work}/"; + if (!Directory.Exists(participantsUrlFIleName)) + { + Directory.CreateDirectory(participantsUrlFIleName); + } + participantsUrlFIleName += imageName; + using (var stream = new FileStream(participantsUrlFIleName, FileMode.Create)) + { + await stream.WriteAsync(images, 0, images.Length); + } + } + + // 添加当日根据【工作内容】分类的照片 + var jobContentUrl = $"{jobContent}/{parm.Content}/"; + if (!Directory.Exists(jobContentUrl)) + { + Directory.CreateDirectory(jobContentUrl); + } + jobContentUrl += imageName; + using (var stream = new FileStream(jobContentUrl, FileMode.Create)) + { + await stream.WriteAsync(images, 0, images.Length); + } + + // 添加当日根据【部门】分类的照片 + var departmentUrl = $"{department}{parm.DeptName}/"; + if (!Directory.Exists(departmentUrl)) + { + Directory.CreateDirectory(departmentUrl); + } + departmentUrl += imageName; + using (var stream = new FileStream(departmentUrl, FileMode.Create)) + { + await stream.WriteAsync(images, 0, images.Length); + } + + var imageUrl = $"{domainUrl}{filePath}/当日照片/{imageName}"; + savedImageUrls.Add(imageUrl); + sortOrder++; + } + + if (savedImageUrls.Count == 0) + { + return ToResponse(ResultCode.CUSTOM_ERROR, "图片上传失败"); + } + + // 保存工作记录 + var modal = parm.Adapt().ToCreate(HttpContext); + modal.CreateTime = DateTime.Now; + modal.UpdateTime = DateTime.Now; + modal.ImageUrl = savedImageUrls[0]; // 第一张图作为封面图 + + int workid; + if (parm.Id != null && parm.Id > 0) + { + await _CamWorkrecordService.UpdateAsync(modal); + workid = (int)parm.Id; + } + else + { + modal = await _CamWorkrecordService.Insertable(modal).ExecuteReturnEntityAsync(); + workid = modal.Id; + } + + // 保存图片记录到新表 + var imageRecords = new List(); + sortOrder = 1; + foreach (var imgUrl in savedImageUrls) + { + imageRecords.Add(new CamWorkrecordImage + { + WorkrecordId = workid, + ImageUrl = imgUrl, + SortOrder = sortOrder++, + CreateTime = DateTime.Now, + UpdateTime = DateTime.Now + }); + } + _CamWorkrecordImageService.AddCamWorkrecordImages(imageRecords); + + // 保存工作人员 + var workers = new List(); + foreach (var item in parm.Workers) + { + var worker = new CamWorker() + { + WorkrecordId = workid, + WorkerName = item, + CreateTime = DateTime.Now, + UpdateTime = DateTime.Now + }; + workers.Add(worker); + } + _CamWorkerService.AsInsertable(workers).ExecuteCommand(); + + return SUCCESS(new { id = workid, imageCount = savedImageUrls.Count }); + } + + /// + /// 删除图片文件(辅助方法) + /// + private async Task DeleteImageFiles(string imageUrl, DateTime? recordTime, List workers, string content, string deptName) + { + if (string.IsNullOrEmpty(imageUrl) || imageUrl.IndexOf("workfiles") == -1) + return; + + try + { + var imagePath = imageUrl.Substring(imageUrl.IndexOf("workfiles")); + var fullImagePath = Path.Combine(WebHostEnvironment.WebRootPath, imagePath); + var fileName = Path.GetFileName(fullImagePath); + + // 删除当日照片 + if (IOFile.Exists(fullImagePath)) + { + IOFile.Delete(fullImagePath); + } + + // 删除参与人员照片 + var participantsPath = Path.Combine(WebHostEnvironment.WebRootPath, "workfiles", recordTime?.ToString("yyyyMM/yyyyMMdd"), "参与人员"); + if (Directory.Exists(participantsPath)) + { + foreach (var workerDir in workers) + { + var workerImagePath = Path.Combine(participantsPath, workerDir.WorkerName, fileName); + if (IOFile.Exists(workerImagePath)) + { + IOFile.Delete(workerImagePath); + } + } + } + + // 删除工作内容照片 + if (!string.IsNullOrEmpty(content)) + { + var jobContentPath = Path.Combine(WebHostEnvironment.WebRootPath, "workfiles", recordTime?.ToString("yyyyMM/yyyyMMdd"), "工作内容", content); + var jobContentImagePath = Path.Combine(jobContentPath, fileName); + if (IOFile.Exists(jobContentImagePath)) + { + IOFile.Delete(jobContentImagePath); + } + } + + // 删除部门照片 + if (!string.IsNullOrEmpty(deptName)) + { + var departmentPath = Path.Combine(WebHostEnvironment.WebRootPath, "workfiles", recordTime?.ToString("yyyyMM/yyyyMMdd"), "部门", deptName); + var departmentImagePath = Path.Combine(departmentPath, fileName); + if (IOFile.Exists(departmentImagePath)) + { + IOFile.Delete(departmentImagePath); + } + } + } + catch (Exception ex) + { + logger.Error($"删除图片文件失败: {imageUrl}, 错误: {ex.Message}"); + } + } + /// /// 删除工作记录 /// @@ -465,7 +741,14 @@ namespace ZR.Admin.WebApi.Controllers try { - // 删除图片文件 + // 删除多图表中的图片文件 + var imageRecords = _CamWorkrecordImageService.GetListByWorkrecordId((int)id); + foreach (var imgRecord in imageRecords) + { + await DeleteImageFiles(imgRecord.ImageUrl, workRecord.RecordTime, workers, workRecord.Content, workRecord.DeptName); + } + + // 删除主图片文件(兼容旧数据) if (!string.IsNullOrEmpty(workRecord.ImageUrl)) { // 从URL中提取文件路径 @@ -540,6 +823,10 @@ namespace ZR.Admin.WebApi.Controllers { logger.Error($"删除文件失败,错误: {ex.Message}"); } + + // 删除图片记录(多图支持) + _CamWorkrecordImageService.DeleteByWorkrecordId((int)id); + // 删除工作人员记录 await _CamWorkerService.DeleteAsync(it => it.WorkrecordId == id); diff --git a/server/Zr.Admin.NET/ZR.Model/Business/CamWorkrecordImage.cs b/server/Zr.Admin.NET/ZR.Model/Business/CamWorkrecordImage.cs new file mode 100644 index 0000000..234fdc5 --- /dev/null +++ b/server/Zr.Admin.NET/ZR.Model/Business/CamWorkrecordImage.cs @@ -0,0 +1,42 @@ + +namespace ZR.Model.Business +{ + /// + /// 工作记录图片 + /// + [SugarTable("cam_workrecord_image")] + public class CamWorkrecordImage + { + /// + /// 主键 + /// + [SugarColumn(IsPrimaryKey = true, IsIdentity = true)] + public long Id { get; set; } + + /// + /// 工作记录ID + /// + public int WorkrecordId { get; set; } + + /// + /// 图片路径 + /// + public string ImageUrl { get; set; } + + /// + /// 排序(1=第一张封面图) + /// + public int SortOrder { get; set; } + + /// + /// 创建时间 + /// + public DateTime? CreateTime { get; set; } + + /// + /// 更新时间 + /// + public DateTime? UpdateTime { get; set; } + + } +} diff --git a/server/Zr.Admin.NET/ZR.Model/Business/Dto/CamWorkrecordDto.cs b/server/Zr.Admin.NET/ZR.Model/Business/Dto/CamWorkrecordDto.cs index 1e1561f..fdc9aa2 100644 --- a/server/Zr.Admin.NET/ZR.Model/Business/Dto/CamWorkrecordDto.cs +++ b/server/Zr.Admin.NET/ZR.Model/Business/Dto/CamWorkrecordDto.cs @@ -63,6 +63,16 @@ namespace ZR.Model.Business.Dto [ExcelColumn(Name = "状态")] public string StatusNameLabel { get; set; } + + /// + /// 图片列表(多图支持) + /// + public List Images { get; set; } + + /// + /// 图片数量 + /// + public int ImageCount { get; set; } } /// @@ -119,9 +129,14 @@ namespace ZR.Model.Business.Dto public string Remarks { get; set; } /// - /// + /// /// public List Workers { get; set; } + + /// + /// 图片列表(多图支持) + /// + public List Images { get; set; } } /// @@ -201,4 +216,35 @@ namespace ZR.Model.Business.Dto } + /// + /// 工作记录图片输出对象 + /// + public class CamWorkrecordImageDto + { + /// + /// 图片ID + /// + public long Id { get; set; } + + /// + /// 工作记录ID + /// + public int WorkrecordId { get; set; } + + /// + /// 图片地址 + /// + public string ImageUrl { get; set; } + + /// + /// 排序号 + /// + public int SortOrder { get; set; } + + /// + /// 创建时间 + /// + public DateTime? CreateTime { get; set; } + } + } \ No newline at end of file diff --git a/server/Zr.Admin.NET/ZR.Service/Business/CamWorkrecordImageService.cs b/server/Zr.Admin.NET/ZR.Service/Business/CamWorkrecordImageService.cs new file mode 100644 index 0000000..d053097 --- /dev/null +++ b/server/Zr.Admin.NET/ZR.Service/Business/CamWorkrecordImageService.cs @@ -0,0 +1,89 @@ +using Infrastructure.Attribute; +using ZR.Model.Business; +using ZR.Repository; +using ZR.Service.Business.IBusinessService; + +namespace ZR.Service.Business +{ + /// + /// 工作记录图片Service业务层处理 + /// + [AppService(ServiceType = typeof(ICamWorkrecordImageService), ServiceLifetime = LifeTime.Transient)] + public class CamWorkrecordImageService : BaseService, ICamWorkrecordImageService + { + /// + /// 根据工作记录ID获取图片列表 + /// + /// + /// + public List GetListByWorkrecordId(int workrecordId) + { + return Queryable() + .Where(x => x.WorkrecordId == workrecordId) + .OrderBy(x => x.SortOrder) + .ToList(); + } + + /// + /// 根据工作记录ID获取图片列表(批量) + /// + /// + /// + public List GetListByWorkrecordIds(List workrecordIds) + { + if (workrecordIds == null || workrecordIds.Count == 0) + return new List(); + + return Queryable() + .Where(x => workrecordIds.Contains(x.WorkrecordId)) + .OrderBy(x => x.WorkrecordId) + .OrderBy(x => x.SortOrder) + .ToList(); + } + + /// + /// 添加工作记录图片 + /// + /// + /// + public CamWorkrecordImage AddCamWorkrecordImage(CamWorkrecordImage model) + { + return Insertable(model).ExecuteReturnEntity(); + } + + /// + /// 批量添加工作记录图片 + /// + /// + /// + public int AddCamWorkrecordImages(List list) + { + if (list == null || list.Count == 0) + return 0; + + return Insertable(list).ExecuteCommand(); + } + + /// + /// 根据工作记录ID删除图片 + /// + /// + /// + public int DeleteByWorkrecordId(int workrecordId) + { + return Delete(x => x.WorkrecordId == workrecordId); + } + + /// + /// 获取工作记录的图片数量 + /// + /// + /// + public int GetImageCount(int workrecordId) + { + return Queryable() + .Where(x => x.WorkrecordId == workrecordId) + .Count(); + } + } +} diff --git a/server/Zr.Admin.NET/ZR.Service/Business/IBusinessService/ICamWorkrecordImageService.cs b/server/Zr.Admin.NET/ZR.Service/Business/IBusinessService/ICamWorkrecordImageService.cs new file mode 100644 index 0000000..43bec37 --- /dev/null +++ b/server/Zr.Admin.NET/ZR.Service/Business/IBusinessService/ICamWorkrecordImageService.cs @@ -0,0 +1,52 @@ +using ZR.Model.Business; + +namespace ZR.Service.Business.IBusinessService +{ + /// + /// 工作记录图片service接口 + /// + public interface ICamWorkrecordImageService : IBaseService + { + /// + /// 根据工作记录ID获取图片列表 + /// + /// + /// + List GetListByWorkrecordId(int workrecordId); + + /// + /// 根据工作记录ID获取图片列表(批量) + /// + /// + /// + List GetListByWorkrecordIds(List workrecordIds); + + /// + /// 添加工作记录图片 + /// + /// + /// + CamWorkrecordImage AddCamWorkrecordImage(CamWorkrecordImage parm); + + /// + /// 批量添加工作记录图片 + /// + /// + /// + int AddCamWorkrecordImages(List list); + + /// + /// 根据工作记录ID删除图片 + /// + /// + /// + int DeleteByWorkrecordId(int workrecordId); + + /// + /// 获取工作记录的图片数量 + /// + /// + /// + int GetImageCount(int workrecordId); + } +} diff --git a/server/Zr.Admin.NET/ZR.Vue/src/views/business/CamWorkrecord.vue b/server/Zr.Admin.NET/ZR.Vue/src/views/business/CamWorkrecord.vue index 240de5f..17905b5 100644 --- a/server/Zr.Admin.NET/ZR.Vue/src/views/business/CamWorkrecord.vue +++ b/server/Zr.Admin.NET/ZR.Vue/src/views/business/CamWorkrecord.vue @@ -80,10 +80,31 @@ align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('deptName')" /> - + @@ -240,14 +261,39 @@ - + +
将文件拖到此处,或点击上传
-
+ +
+
+ 预览图片 + + + {{ idx + 1 }} +
+
+ +
预览图片
@@ -451,6 +497,7 @@ function reset() { id: null, deptName: null, imageUrl: null, + images: [], // 多图支持 recordTime: null, longitude: null, latitude: null, @@ -465,7 +512,7 @@ function reset() { proxy.resetForm('formRef') } -// 处理图片上传,转换为Base64 +// 处理图片上传,转换为Base64(支持多图) function handleImageChange(file) { const isImage = file.raw.type.startsWith('image/') const isLt10M = file.raw.size / 1024 / 1024 < 10 @@ -479,11 +526,42 @@ function handleImageChange(file) { return } + // 检查图片数量限制 + if (!form.value.images) { + form.value.images = [] + } + if (form.value.images.length >= 15) { + proxy.$modal.msgError('最多只能上传15张图片!') + return + } + const reader = new FileReader() reader.readAsDataURL(file.raw) reader.onload = (e) => { - form.value.imageUrl = e.target.result - form.value.image = e.target.result + // 添加到图片数组 + form.value.images.push({ + imageUrl: e.target.result, + isNew: true // 标记为新上传的图片 + }) + // 第一张图作为封面 + if (form.value.images.length === 1) { + form.value.imageUrl = e.target.result + form.value.image = e.target.result + } + } +} + +// 删除图片 +function removeImage(index) { + if (form.value.images && form.value.images.length > index) { + form.value.images.splice(index, 1) + // 更新封面图 + if (form.value.images.length > 0) { + form.value.imageUrl = form.value.images[0].imageUrl || form.value.images[0] + } else { + form.value.imageUrl = null + form.value.image = null + } } } @@ -539,12 +617,8 @@ function handlePreview(row) { title.value = '查看' opertype.value = 3 form.value = { - ...data - } - - // 如果存在图片地址,转换为Base64用于显示 - if (data.imageUrl) { - convertImageUrlToBase64(data.imageUrl) + ...data, + images: data.images || [] // 确保 images 数组存在 } } }) @@ -571,12 +645,8 @@ function handleUpdate(row) { opertype.value = 2 form.value = { - ...data - } - - // 如果存在图片地址,转换为Base64 - if (data.imageUrl) { - convertImageUrlToBase64(data.imageUrl) + ...data, + images: data.images || [] // 确保 images 数组存在 } } }) @@ -692,10 +762,60 @@ handleQuery() // 图片容器样式 .image-container { display: flex; + flex-direction: column; justify-content: center; align-items: center; padding: 8px; + .multi-image-wrapper { + display: flex; + flex-wrap: wrap; + gap: 4px; + justify-content: center; + align-items: center; + position: relative; + } + + .table-image-small { + width: 50px; + height: 50px; + border-radius: 4px; + object-fit: cover; + border: 1px solid #e4e7ed; + transition: all 0.3s ease; + + &:hover { + transform: scale(1.1); + border-color: #409eff; + box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3); + z-index: 10; + } + } + + .image-count-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 30px; + height: 30px; + background: linear-gradient(135deg, #409eff 0%, #36a3f7 100%); + color: white; + border-radius: 50%; + font-size: 11px; + font-weight: bold; + } + + .image-count-text { + margin-top: 4px; + font-size: 11px; + color: #909399; + } + + .no-image { + color: #c0c4cc; + font-size: 12px; + } + .table-image { width: 80px; height: 80px; @@ -970,6 +1090,62 @@ handleQuery() } } +// 多图预览样式 +.multi-image-preview { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 12px; + padding: 12px; + background: #f5f7fa; + border-radius: 8px; + border: 1px dashed #dcdfe6; + + .preview-item { + position: relative; + width: 120px; + height: 120px; + border-radius: 6px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + + .delete-btn { + opacity: 1; + } + } + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + + .delete-btn { + position: absolute; + top: 4px; + right: 4px; + opacity: 0; + transition: opacity 0.3s ease; + } + + .image-index { + position: absolute; + bottom: 4px; + left: 4px; + background: rgba(0, 0, 0, 0.6); + color: white; + font-size: 11px; + padding: 2px 6px; + border-radius: 10px; + } + } +} + // 响应式设计 @media (max-width: 1200px) { .work-record-table { diff --git a/server/Zr.Admin.NET/document/sql server/cam_workrecord_image.sql b/server/Zr.Admin.NET/document/sql server/cam_workrecord_image.sql new file mode 100644 index 0000000..800d2be --- /dev/null +++ b/server/Zr.Admin.NET/document/sql server/cam_workrecord_image.sql @@ -0,0 +1,53 @@ +USE [WatermarkCamera] +GO + +/****** Object: Table [dbo].[cam_workrecord_image] Script Date: 2025/12/27 ******/ +SET ANSI_NULLS ON +GO + +SET QUOTED_IDENTIFIER ON +GO + +-- 创建工作记录图片表 +CREATE TABLE [dbo].[cam_workrecord_image]( + [Id] [bigint] IDENTITY(1,1) NOT NULL, + [WorkrecordId] [int] NOT NULL, + [ImageUrl] [nvarchar](500) NOT NULL, + [SortOrder] [int] NOT NULL DEFAULT 1, + [CreateTime] [datetime] NOT NULL DEFAULT (getdate()), + [UpdateTime] [datetime] NOT NULL DEFAULT (getdate()), + CONSTRAINT [PK_cam_workrecord_image] PRIMARY KEY CLUSTERED ([Id] ASC) + WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] +) ON [PRIMARY] +GO + +-- 添加外键约束(删除工作记录时级联删除图片) +ALTER TABLE [dbo].[cam_workrecord_image] +ADD CONSTRAINT [FK_cam_workrecord_image_workrecord] +FOREIGN KEY ([WorkrecordId]) REFERENCES [dbo].[cam_workrecord]([Id]) +ON DELETE CASCADE +GO + +-- 添加索引(按工作记录ID查询) +CREATE NONCLUSTERED INDEX [IX_cam_workrecord_image_WorkrecordId] +ON [dbo].[cam_workrecord_image] ([WorkrecordId] ASC) +GO + +-- 添加字段说明 +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'主键' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'cam_workrecord_image', @level2type=N'COLUMN',@level2name=N'Id' +GO + +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'工作记录ID' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'cam_workrecord_image', @level2type=N'COLUMN',@level2name=N'WorkrecordId' +GO + +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'图片路径' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'cam_workrecord_image', @level2type=N'COLUMN',@level2name=N'ImageUrl' +GO + +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'排序(1=第一张封面图)' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'cam_workrecord_image', @level2type=N'COLUMN',@level2name=N'SortOrder' +GO + +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'创建时间' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'cam_workrecord_image', @level2type=N'COLUMN',@level2name=N'CreateTime' +GO + +EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'更新时间' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'cam_workrecord_image', @level2type=N'COLUMN',@level2name=N'UpdateTime' +GO diff --git a/server/Zr.Admin.NET/document/sql server/cam_workrecord_image_migrate.sql b/server/Zr.Admin.NET/document/sql server/cam_workrecord_image_migrate.sql new file mode 100644 index 0000000..79cfd66 --- /dev/null +++ b/server/Zr.Admin.NET/document/sql server/cam_workrecord_image_migrate.sql @@ -0,0 +1,120 @@ +USE [WatermarkCamera] +GO + +/* + * v1.0.5 老数据迁移脚本 + * 功能:将 cam_workrecord.ImageUrl 迁移到 cam_workrecord_image 表 + * + * 执行前提: + * 1. 已执行 cam_workrecord_image.sql 建表脚本 + * 2. 建议先备份数据库 + * + * 执行顺序: + * 1. 先执行 cam_workrecord_image.sql + * 2. 再执行本脚本 + */ + +-- ============================================= +-- 第一步:检查表是否存在 +-- ============================================= +IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[cam_workrecord_image]') AND type in (N'U')) +BEGIN + RAISERROR('错误:cam_workrecord_image 表不存在,请先执行建表脚本!', 16, 1) + RETURN +END +GO + +-- ============================================= +-- 第二步:迁移前数据统计 +-- ============================================= +PRINT '========== 迁移前数据统计 ==========' + +DECLARE @count_before INT +SELECT @count_before = COUNT(*) FROM [dbo].[cam_workrecord] WHERE [ImageUrl] IS NOT NULL AND [ImageUrl] <> '' +PRINT 'cam_workrecord 有图片的记录数:' + CAST(@count_before AS VARCHAR(10)) + +DECLARE @count_image_before INT +SELECT @count_image_before = COUNT(*) FROM [dbo].[cam_workrecord_image] +PRINT 'cam_workrecord_image 现有记录数:' + CAST(@count_image_before AS VARCHAR(10)) +GO + +-- ============================================= +-- 第三步:执行数据迁移 +-- ============================================= +PRINT '========== 开始数据迁移 ==========' + +-- 迁移老数据:将 cam_workrecord.ImageUrl 迁移到 cam_workrecord_image 表 +-- 只迁移尚未迁移的数据(避免重复迁移) +INSERT INTO [dbo].[cam_workrecord_image] + ([WorkrecordId], [ImageUrl], [SortOrder], [CreateTime], [UpdateTime]) +SELECT + w.[Id] AS WorkrecordId, + w.[ImageUrl], + 1 AS SortOrder, -- 老数据都是第一张(封面图) + ISNULL(w.[CreateTime], GETDATE()), + ISNULL(w.[UpdateTime], GETDATE()) +FROM [dbo].[cam_workrecord] w +WHERE w.[ImageUrl] IS NOT NULL + AND w.[ImageUrl] <> '' + AND NOT EXISTS ( + SELECT 1 FROM [dbo].[cam_workrecord_image] img + WHERE img.[WorkrecordId] = w.[Id] + ) + +DECLARE @migrated INT = @@ROWCOUNT +PRINT '本次迁移记录数:' + CAST(@migrated AS VARCHAR(10)) +GO + +-- ============================================= +-- 第四步:迁移后数据验证 +-- ============================================= +PRINT '========== 迁移后数据验证 ==========' + +-- 验证1:对比数量 +SELECT + '迁移验证' AS [类型], + (SELECT COUNT(*) FROM [dbo].[cam_workrecord] WHERE [ImageUrl] IS NOT NULL AND [ImageUrl] <> '') AS [cam_workrecord有图片数], + (SELECT COUNT(*) FROM [dbo].[cam_workrecord_image]) AS [cam_workrecord_image记录数] + +-- 验证2:检查是否有遗漏 +DECLARE @missing INT +SELECT @missing = COUNT(*) +FROM [dbo].[cam_workrecord] w +WHERE w.[ImageUrl] IS NOT NULL + AND w.[ImageUrl] <> '' + AND NOT EXISTS ( + SELECT 1 FROM [dbo].[cam_workrecord_image] img + WHERE img.[WorkrecordId] = w.[Id] + ) + +IF @missing > 0 +BEGIN + PRINT '警告:有 ' + CAST(@missing AS VARCHAR(10)) + ' 条记录未迁移成功!' +END +ELSE +BEGIN + PRINT '验证通过:所有数据迁移成功!' +END +GO + +-- ============================================= +-- 第五步:显示迁移结果样例 +-- ============================================= +PRINT '========== 迁移结果样例(前10条) ==========' + +SELECT TOP 10 + w.[Id] AS [WorkrecordId], + w.[Content] AS [工作内容], + w.[ImageUrl] AS [原ImageUrl], + img.[Id] AS [新ImageId], + img.[ImageUrl] AS [新ImageUrl], + img.[SortOrder] AS [排序] +FROM [dbo].[cam_workrecord] w +INNER JOIN [dbo].[cam_workrecord_image] img ON w.[Id] = img.[WorkrecordId] +ORDER BY w.[Id] DESC +GO + +PRINT '========== 迁移完成 ==========' +PRINT '注意:cam_workrecord.ImageUrl 字段已保留,用于兼容老代码。' +PRINT '后续新数据会同时写入 cam_workrecord.ImageUrl(封面图)和 cam_workrecord_image(所有图片)。' +GO diff --git a/uniapp/WorkCameraf/common/server.js b/uniapp/WorkCameraf/common/server.js index f83be57..4327af6 100644 --- a/uniapp/WorkCameraf/common/server.js +++ b/uniapp/WorkCameraf/common/server.js @@ -23,4 +23,16 @@ export const addWatermarkRecord = async (data) => { console.log(url, data); const res = await post(url, data); return res; +} + +/** + * 添加工作记录(多图支持 v2) + * @param {Object} data - 包含 Images 数组的数据 + * @returns + */ +export const addWatermarkRecordV2 = async (data) => { + var url = base_url + "addworkrecordv2"; + console.log(url, data); + const res = await post(url, data); + return res; } \ No newline at end of file diff --git a/uniapp/WorkCameraf/common/utils.js b/uniapp/WorkCameraf/common/utils.js index 4af6953..f2243cb 100644 --- a/uniapp/WorkCameraf/common/utils.js +++ b/uniapp/WorkCameraf/common/utils.js @@ -1,8 +1,53 @@ /** * 获取位置的异步函数 + * H5 端会尝试使用浏览器定位,失败则返回模拟数据 */ export const getLocation = async () => { return new Promise((resolve, reject) => { + // #ifdef H5 + // H5 端:尝试使用浏览器定位,失败则使用模拟数据 + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + (position) => { + resolve({ + latitude: position.coords.latitude, + longitude: position.coords.longitude, + accuracy: position.coords.accuracy, + altitude: position.coords.altitude || 0 + }); + }, + (error) => { + console.warn('H5 定位失败,使用模拟数据:', error.message); + // 返回模拟的经纬度(默认:北京天安门附近) + resolve({ + latitude: 39.908823, + longitude: 116.397470, + accuracy: 100, + altitude: 0, + _isMock: true // 标记为模拟数据 + }); + }, + { + enableHighAccuracy: true, + timeout: 5000, + maximumAge: 0 + } + ); + } else { + console.warn('浏览器不支持定位,使用模拟数据'); + // 返回模拟的经纬度 + resolve({ + latitude: 39.908823, + longitude: 116.397470, + accuracy: 100, + altitude: 0, + _isMock: true + }); + } + // #endif + + // #ifndef H5 + // App/小程序端:使用原生定位 uni.getLocation({ isHighAccuracy: true, altitude: true, @@ -11,17 +56,31 @@ export const getLocation = async () => { resolve(res); }, fail: (err) => { - reject(err); + console.error('定位失败:', err); + // App端定位失败也返回模拟数据,方便调试 + resolve({ + latitude: 39.908823, + longitude: 116.397470, + accuracy: 100, + altitude: 0, + _isMock: true + }); } }); + // #endif }); } -export const chooseImage = async () => { +/** + * 选择图片(支持多选) + * @param {number} count - 最多选择的图片数量,默认1 + * @returns {Promise} - 返回选择的图片信息 + */ +export const chooseImage = async (count = 1) => { return new Promise((resolve, reject) => { uni.chooseImage({ - sourceType:['camera'], - count: 1, + sourceType: ['camera', 'album'], // 支持拍照和相册选择 + count: count, // 支持多选 success: (res) => { resolve(res); }, diff --git a/uniapp/WorkCameraf/pages/index/index.vue b/uniapp/WorkCameraf/pages/index/index.vue index 285f811..e270f3e 100644 --- a/uniapp/WorkCameraf/pages/index/index.vue +++ b/uniapp/WorkCameraf/pages/index/index.vue @@ -37,11 +37,39 @@ - 图片预览 - + 图片预览 ({{ imageList.length }}/{{ MAX_IMAGES }}) + + + + + + + × + + {{ index + 1 }} + + + + + + + 继续拍摄 + + + + + @@ -176,7 +204,7 @@ import { getLocationTranslate, getLocationGeocoder, } from "../../common/mapTranslateResult"; -import { getConfig, addWatermarkRecord } from "../../common/server"; +import { getConfig, addWatermarkRecord, addWatermarkRecordV2 } from "../../common/server"; import { onLoad } from "@dcloudio/uni-app"; // 简单初始化 // window.erudaInstance = eruda.init(); @@ -203,6 +231,9 @@ const locations = ref({ // 图片相关 const imageSrc = ref(""); const originalImageSrc = ref(""); +// 多图支持 +const imageList = ref([]); // 存储多张图片 [{original, watermark, sizeInfo}] +const MAX_IMAGES = 15; // 最大图片数量 const imageSizeInfo = ref({ original: { width: 0, @@ -260,13 +291,22 @@ const handleStartCapture = async () => { locationInfo.value = geocoderResult || "未知位置"; currentTime.value = new Date(); - // 选择图片 - const image = await chooseImage(); + // 选择图片(支持多选) + const image = await chooseImage(9); // 最多选择9张 console.log("图片", image); - originalImageSrc.value = image.tempFilePaths[0]; - imageSrc.value = image.tempFilePaths[0]; - await addWatermarkToImage(); + // 处理多张图片 + 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) { @@ -279,13 +319,100 @@ const handleStartCapture = async () => { } }; -// 预览图片 -const handlePreviewImage = () => { - uni.previewImage({ - urls: [imageSrc.value], +// 添加更多图片 +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) => { + if (imageList.value.length > index) { + imageList.value.splice(index, 1); + // 更新当前显示的图片 + if (imageList.value.length > 0) { + const newIndex = Math.min(index, imageList.value.length - 1); + imageSrc.value = imageList.value[newIndex].watermark; + originalImageSrc.value = imageList.value[newIndex].original; + } else { + imageSrc.value = ""; + originalImageSrc.value = ""; + } + } +}; + +// 预览指定索引的图片 +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], + }); + } +}; + // 表单事件处理 const handleComboxSelect = async (value) => { console.log("选择的工作内容:", value); @@ -432,8 +559,44 @@ const handleRetakeCancel = async () => { statusIndex.value = 0; deptIndex.value = 0; workContent.value = ""; + imageList.value = []; // 清空图片列表 loadCandidates(); - await addWatermarkToImage(); + 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(); }; // 保存并提交 @@ -448,6 +611,15 @@ const handleSaveAndSubmit = async () => { return; } + // 检查是否有图片 + if (imageList.value.length === 0) { + uni.showToast({ + title: "请先拍摄图片", + icon: "error", + }); + return; + } + isSubmitting.value = true; try { @@ -455,15 +627,15 @@ const handleSaveAndSubmit = async () => { title: "保存中...", }); - // 先添加水印 - await addWatermarkToImage(); + // 刷新所有图片的水印(确保最新信息) + 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(), // 使用ISO格式确保时间格式一致 + date: new Date().toISOString(), }; console.log("locationData", locationData); @@ -471,24 +643,24 @@ const handleSaveAndSubmit = async () => { 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); + // 转换所有图片为 Base64 + const imagesBase64 = []; + for (const img of imageList.value) { + const base64 = await imageToBase64(img.watermark); + imagesBase64.push(base64); + } + + console.log(`共${imagesBase64.length}张图片`); + const camRecordWorkDto = { // 部门名称 DeptName: departments.value[deptIndex.value], - // 图片地址 - Image: imageBase64, + // 图片地址(第一张作为封面,兼容旧版) + Image: imagesBase64[0], + // 多图支持 + Images: imagesBase64, // 工作记录时间 RecordTime: formatDate(currentTime.value), // 经度 @@ -503,11 +675,12 @@ const handleSaveAndSubmit = async () => { StatusName: statusList.value[statusIndex.value], // 备注 Remarks: _remarks, - // 工作人员列表 Workers: workers.value, }; - var res = await addWatermarkRecord(camRecordWorkDto); + + // 使用 v2 API 支持多图 + var res = await addWatermarkRecordV2(camRecordWorkDto); console.log(res); if (res.code != 200) { @@ -610,6 +783,7 @@ const validateFormData = (isSubmit = false) => { const resetFormData = () => { imageSrc.value = ""; originalImageSrc.value = ""; + imageList.value = []; // 清空图片列表 locationInfo.value = ""; currentTime.value = ""; imageSizeInfo.value = { @@ -731,6 +905,88 @@ onLoad(async () => { 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); +} + +.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%;