u
This commit is contained in:
1 parent
f9eaacf02a
commit
69eda8b8ad
27 files changed
+2797
-616
No files matched your search
@@ -19,14 +19,14 @@ public class FileConfig {
|
||||
private String staticPath;
|
||||
|
||||
public String getCommonPath() {
|
||||
return PathUtils.joinFilePath(commonDir, commonDir);
|
||||
return PathUtils.joinFilePath(baseDir, commonDir);
|
||||
}
|
||||
|
||||
public String getContainerPath() {
|
||||
return PathUtils.joinFilePath(commonDir, containerDir);
|
||||
return PathUtils.joinFilePath(baseDir, containerDir);
|
||||
}
|
||||
|
||||
public String getTempPath() {
|
||||
return PathUtils.joinFilePath(commonDir, tempDir);
|
||||
return PathUtils.joinFilePath(baseDir, tempDir);
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,6 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler(fileConfig.getStaticPath() + "/**")
|
||||
.addResourceLocations("file:" + fileConfig.getUploadDir() + "/");
|
||||
.addResourceLocations("file:" + fileConfig.getBaseDir() + "/");
|
||||
}
|
||||
}
|
||||
+152
-110
@@ -14,10 +14,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import javax.annotation.Resource;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/file")
|
||||
@@ -25,8 +22,6 @@ public class FileController {
|
||||
@Resource
|
||||
private DbUtils dbUtils;
|
||||
@Resource
|
||||
private FileUploadUtils fileUploadUtils;
|
||||
@Resource
|
||||
private FileConfig fileConfig;
|
||||
|
||||
/**
|
||||
@@ -34,9 +29,9 @@ public class FileController {
|
||||
*/
|
||||
@PostMapping("/uploadTemp")
|
||||
public Result<Map<String, String>> uploadTemp(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "fileName", required = false) String fileName)
|
||||
throws Exception{
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "fileName", required = false) String fileName)
|
||||
throws Exception {
|
||||
// 1. 校验
|
||||
FileUtils.checkFile(file);
|
||||
|
||||
@@ -46,6 +41,7 @@ public class FileController {
|
||||
// 3. 物理路径
|
||||
String basePath = fileConfig.getTempPath(); // D:/data/file/temp/
|
||||
|
||||
System.out.println(basePath);
|
||||
// 4. 保存
|
||||
FileUtils.saveFile(file, basePath, uuidName);
|
||||
|
||||
@@ -69,10 +65,7 @@ public class FileController {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Map<String, Object>> uploadCommon(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("moduleId") String moduleId,
|
||||
@RequestParam("father") Long father,
|
||||
@RequestParam("fileName") String fileName,
|
||||
@RequestParam(value = "mxXh", required = false) Integer mxXh
|
||||
@RequestParam("fileName") String fileName
|
||||
) throws Exception {
|
||||
// 1. 校验
|
||||
FileUtils.checkFile(file);
|
||||
@@ -97,22 +90,6 @@ public class FileController {
|
||||
uuidName
|
||||
);
|
||||
|
||||
// 保存到数据库
|
||||
String fileSuffix = FileUtils.getSuffix(fileName);
|
||||
long fileSize = FileUtils.getFileSize(file);
|
||||
|
||||
Map<String, Object> insertMap = new HashMap<>();
|
||||
insertMap.put("subid", UniqueId.nextId());
|
||||
insertMap.put("father", father);
|
||||
insertMap.put("mx_moduleid", moduleId);
|
||||
insertMap.put("mx_xh", Optional.ofNullable(mxXh).orElse(0));
|
||||
insertMap.put("mx_filename", fileName);
|
||||
insertMap.put("mx_filesize", fileSize);
|
||||
insertMap.put("mx_fileext", fileSuffix);
|
||||
insertMap.put("mx_mapfilename", relativePath);
|
||||
|
||||
dbUtils.insert("bf_file", "subid", insertMap);
|
||||
|
||||
// 7. URL
|
||||
String url = PathUtils.joinUrl(
|
||||
fileConfig.getServerUrl(),
|
||||
@@ -129,127 +106,192 @@ public class FileController {
|
||||
return Result.ok(result);
|
||||
}
|
||||
|
||||
// @PostMapping("/uploadContainer")
|
||||
// @Transactional(rollbackFor = Exception.class)
|
||||
// public Result<Map<String, Object>> uploadContainer(
|
||||
// @RequestParam("file") MultipartFile file,
|
||||
// @RequestParam("containerNo") String containerNo,
|
||||
// @RequestParam("fileName") String fileName,
|
||||
// @RequestParam("jobId") String jobId
|
||||
// ) throws Exception {
|
||||
// // 1. 校验
|
||||
// FileUtils.checkFile(file);
|
||||
//
|
||||
// // 2. 日期路径
|
||||
// String datePath = TimeUtils.todayPath(); // 2026/03/30/
|
||||
//
|
||||
// // 3. 根路径
|
||||
// String basePath = fileConfig.getContainerPath();
|
||||
//
|
||||
// // 4. 构建目录:日期 / 箱号 / jobId
|
||||
// String fullDirPath = PathUtils.joinFilePath(
|
||||
// basePath,
|
||||
// datePath,
|
||||
// containerNo,
|
||||
// jobId
|
||||
// );
|
||||
//
|
||||
// // 5. 创建目录(幂等)
|
||||
// File dir = new File(fullDirPath);
|
||||
// if (!dir.exists()) {
|
||||
// dir.mkdirs();
|
||||
// }
|
||||
//
|
||||
// // 6. 文件后缀
|
||||
// String suffix = FileUtils.getSuffix(fileName);
|
||||
//
|
||||
// long nextId = UniqueId.nextId();
|
||||
//
|
||||
// // 7. 使用雪花ID作为文件名(核心优化)
|
||||
// String newFileName = nextId + suffix;
|
||||
//
|
||||
// // 8. 保存文件
|
||||
// FileUtils.saveFile(file, fullDirPath, newFileName);
|
||||
//
|
||||
// // 9. 相对路径
|
||||
// String relativePath = PathUtils.joinFilePath(
|
||||
// fileConfig.getContainerDir(),
|
||||
// datePath,
|
||||
// containerNo,
|
||||
// jobId,
|
||||
// newFileName
|
||||
// );
|
||||
//
|
||||
// // 10. URL
|
||||
// String url = PathUtils.joinUrl(
|
||||
// fileConfig.getServerUrl(),
|
||||
// fileConfig.getStaticPath(),
|
||||
// relativePath
|
||||
// );
|
||||
//
|
||||
// // 11. 返回
|
||||
// Map<String, Object> result = new HashMap<>();
|
||||
// result.put("url", url);
|
||||
// result.put("path", relativePath);
|
||||
// result.put("uid", nextId);
|
||||
// result.put("fileName", newFileName);
|
||||
//
|
||||
// return Result.ok(result);
|
||||
// }
|
||||
|
||||
@PostMapping("/uploadContainer")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Map<String, Object>> uploadContainer(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("containerNo") String containerNo
|
||||
@RequestParam("fileName") String fileName
|
||||
) throws Exception {
|
||||
|
||||
// 1. 校验
|
||||
FileUtils.checkFile(file);
|
||||
|
||||
// 2. 日期路径
|
||||
String datePath = TimeUtils.todayPath(); // 2026/03/29/
|
||||
String datePath = TimeUtils.todayPath(); // 2026/03/30/
|
||||
|
||||
// 3. 根路径
|
||||
String basePath = fileConfig.getContainerPath();
|
||||
String dayPath = PathUtils.joinFilePath(basePath, datePath);
|
||||
|
||||
String relativePath;
|
||||
String fileName;
|
||||
String containerDirName;
|
||||
// 4. 雪花 ID(唯一)
|
||||
long nextId = UniqueId.nextId();
|
||||
|
||||
// 🔥 4. 并发锁(同一天 + container)
|
||||
String lockKey = (datePath + containerNo).intern();
|
||||
// 5. 分桶(0-ff)
|
||||
String bucket = String.format("%02x", nextId & 0xff);
|
||||
|
||||
synchronized (lockKey) {
|
||||
// 6. 构建目录:日期 + 分桶
|
||||
String fullDirPath = PathUtils.joinFilePath(
|
||||
basePath,
|
||||
datePath,
|
||||
bucket
|
||||
);
|
||||
|
||||
// 4.1 获取 container 目录(防重复)
|
||||
containerDirName = getAvailableContainerDir(dayPath, containerNo);
|
||||
|
||||
String fullDirPath = PathUtils.joinFilePath(dayPath, containerDirName);
|
||||
|
||||
// 4.2 获取下一个序号(不补位)
|
||||
int index = getNextIndex(fullDirPath);
|
||||
|
||||
// 4.3 后缀
|
||||
String suffix = FileUtils.getSuffix(file.getOriginalFilename());
|
||||
|
||||
// 4.4 文件名(核心规则)
|
||||
fileName = containerNo + "_" + String.format("%02d", index) + suffix;
|
||||
|
||||
// 4.5 保存
|
||||
FileUtils.saveFile(file, fullDirPath, fileName);
|
||||
|
||||
// 4.6 相对路径
|
||||
relativePath = PathUtils.joinFilePath(
|
||||
fileConfig.getContainerDir(),
|
||||
datePath,
|
||||
containerDirName,
|
||||
fileName
|
||||
);
|
||||
// 7. 创建目录(幂等)
|
||||
File dir = new File(fullDirPath);
|
||||
if (!dir.exists()) {
|
||||
dir.mkdirs();
|
||||
}
|
||||
|
||||
// 5. URL
|
||||
// 8. 文件后缀
|
||||
String suffix = FileUtils.getSuffix(fileName);
|
||||
|
||||
String newFileName = nextId + suffix;
|
||||
|
||||
// 9. 保存文件
|
||||
FileUtils.saveFile(file, fullDirPath, newFileName);
|
||||
|
||||
// 10. 相对路径
|
||||
String relativePath = PathUtils.joinFilePath(
|
||||
fileConfig.getContainerDir(),
|
||||
datePath,
|
||||
bucket,
|
||||
newFileName
|
||||
);
|
||||
|
||||
// 11. URL
|
||||
String url = PathUtils.joinUrl(
|
||||
fileConfig.getServerUrl(),
|
||||
fileConfig.getStaticPath(),
|
||||
relativePath
|
||||
);
|
||||
|
||||
// 6. 返回
|
||||
// 12. 返回
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("url", url);
|
||||
result.put("path", relativePath);
|
||||
result.put("fileName", fileName);
|
||||
result.put("containerDir", containerDirName);
|
||||
result.put("uid", nextId);
|
||||
result.put("fileName", newFileName);
|
||||
result.put("bucket", bucket);
|
||||
|
||||
return Result.ok(result);
|
||||
}
|
||||
|
||||
public static int getNextIndex(String dirPath) {
|
||||
File dir = new File(dirPath);
|
||||
@PostMapping("/delete")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> deleteFile(@RequestBody Map<String, Object> params) {
|
||||
try {
|
||||
// 1. 参数校验
|
||||
ValidationUtils.checkRequiredFields(params, "subid");
|
||||
|
||||
if (!dir.exists()) {
|
||||
return 1;
|
||||
}
|
||||
Long subid = Long.valueOf(params.get("subid").toString());
|
||||
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null || files.length == 0) {
|
||||
return 1;
|
||||
}
|
||||
// 2. 查询文件记录
|
||||
List<Map<String, Object>> maps = dbUtils.loadData(
|
||||
"SELECT * FROM bf_file WHERE subid = ?", subid
|
||||
);
|
||||
|
||||
int max = 0;
|
||||
|
||||
for (File f : files) {
|
||||
String name = f.getName();
|
||||
|
||||
// 解析 xxx_01.jpg
|
||||
int underscore = name.lastIndexOf("_");
|
||||
int dot = name.lastIndexOf(".");
|
||||
|
||||
if (underscore > -1 && dot > underscore) {
|
||||
try {
|
||||
int num = Integer.parseInt(name.substring(underscore + 1, dot));
|
||||
max = Math.max(max, num);
|
||||
} catch (Exception ignored) {}
|
||||
if (maps.isEmpty()) {
|
||||
return Result.ok();
|
||||
}
|
||||
}
|
||||
|
||||
return max + 1;
|
||||
}
|
||||
Map<String, Object> objectMap = maps.get(0);
|
||||
|
||||
public static String getAvailableContainerDir(String basePath, String containerNo) {
|
||||
File baseDir = new File(basePath);
|
||||
// 3. 获取文件路径
|
||||
String mxMapfilename = objectMap.get("mx_mapfilename") == null
|
||||
? ""
|
||||
: objectMap.get("mx_mapfilename").toString();
|
||||
|
||||
if (!baseDir.exists()) {
|
||||
baseDir.mkdirs();
|
||||
}
|
||||
|
||||
File dir = new File(baseDir, containerNo);
|
||||
if (!dir.exists()) {
|
||||
return containerNo;
|
||||
}
|
||||
|
||||
int i = 1;
|
||||
while (true) {
|
||||
String newName = containerNo + "_" + String.format("%02d", i);
|
||||
File newDir = new File(baseDir, newName);
|
||||
if (!newDir.exists()) {
|
||||
return newName;
|
||||
if (StringUtils.isBlank(mxMapfilename)) {
|
||||
return Result.fail("文件路径为空");
|
||||
}
|
||||
i++;
|
||||
|
||||
// 4. 拼接真实路径
|
||||
String filePath = PathUtils.joinFilePath(
|
||||
fileConfig.getBaseDir(),
|
||||
mxMapfilename
|
||||
);
|
||||
|
||||
// 5. 删除本地文件
|
||||
boolean deleted = FileUtils.deleteFileByPath(filePath);
|
||||
if (!deleted) {
|
||||
System.out.println("文件删除失败或不存在: " + filePath);
|
||||
}
|
||||
|
||||
// 6. 删除数据库
|
||||
dbUtils.deleteById("bf_file", "subid", subid);
|
||||
|
||||
return Result.ok();
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return Result.fail("删除失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -68,6 +68,7 @@ public class OcrController {
|
||||
|
||||
return Result.ok(result);
|
||||
} catch (Exception e){
|
||||
e.printStackTrace();
|
||||
return Result.fail("OCR 识别失败,请联系管理员");
|
||||
}
|
||||
}
|
||||
|
||||
+63
-52
@@ -41,67 +41,78 @@ public class OssController {
|
||||
ValidationUtils.checkRequiredFields(params, "father", "mx_xh", "mx_filename", "mx_filesize", "mx_mapfilename", "mx_moduleid");
|
||||
|
||||
String userId = UserContext.get().getUserId();
|
||||
Object subIdObj = params.get("subid");
|
||||
|
||||
long nextId = UniqueId.nextId();
|
||||
Long subid = null;
|
||||
|
||||
params.put("subid", nextId);
|
||||
if (subIdObj != null) {
|
||||
subid = Long.valueOf(subIdObj.toString());
|
||||
}
|
||||
|
||||
// 如果 subid 为空,生成新ID
|
||||
if (subid == null) {
|
||||
long nextId = UniqueId.nextId();
|
||||
subid = nextId;
|
||||
}
|
||||
|
||||
params.put("subid", subid);
|
||||
params.put("mx_inputuser_id",userId);
|
||||
params.put("mx_inputdatetime", TimeUtils.getDateTime());
|
||||
|
||||
dbUtils.insert("bf_file","subid",params);
|
||||
|
||||
return Result.ok(nextId);
|
||||
return Result.ok(subid);
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> deleteFile(@RequestBody Map<String, Object> params) {
|
||||
ValidationUtils.checkRequiredFields(params, "subid", "tableName", "bucketName");
|
||||
|
||||
String tableName = params.get("tableName").toString();
|
||||
String bucketName = params.get("bucketName").toString();
|
||||
String id = params.get("subid").toString();
|
||||
|
||||
// 查询数据
|
||||
List<Map<String, Object>> maps = dbUtils.loadData("SELECT * FROM " + tableName + " WHERE subid = ?", id);
|
||||
|
||||
// 如果没有数据,直接返回删除成功
|
||||
if (ObjectUtils.isEmpty(maps)) {
|
||||
return Result.ok("删除成功");
|
||||
}
|
||||
|
||||
String mxMapfilename = maps.get(0).get("mx_mapfilename").toString();
|
||||
String orgId = UserContext.get().getOrgId();
|
||||
int index = mxMapfilename.indexOf(orgId + "/");
|
||||
|
||||
// 如果没找到,就转换orgId为大写
|
||||
if (index < 0) {
|
||||
orgId = orgId.toUpperCase();
|
||||
index = mxMapfilename.indexOf(orgId + "/");
|
||||
}
|
||||
|
||||
String objectName = mxMapfilename.substring(index);
|
||||
|
||||
// 删除记录
|
||||
Map<String, Object> deleteParams = new HashMap<>();
|
||||
deleteParams.put("subid", id);
|
||||
int deleteCount = dbUtils.delete(tableName, deleteParams);
|
||||
|
||||
// 如果删除成功,发送删除请求
|
||||
if (deleteCount > 0) {
|
||||
if(StringUtils.isBlank(objectName)){
|
||||
return Result.fail("删除失败,文件格式有误");
|
||||
}
|
||||
String requestUrl = String.format("http://g3oss.g3soft.cn:8082/delete?bucketName=%s&objectName=%s", bucketName, objectName);
|
||||
try {
|
||||
HttpUtils.get(requestUrl);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("删除文件失败",e);
|
||||
}
|
||||
}
|
||||
|
||||
return Result.ok();
|
||||
}
|
||||
// @PostMapping("/delete")
|
||||
// @Transactional(rollbackFor = Exception.class)
|
||||
// public Result<Void> deleteFile(@RequestBody Map<String, Object> params) {
|
||||
// ValidationUtils.checkRequiredFields(params, "subid", "tableName", "bucketName");
|
||||
//
|
||||
// String tableName = params.get("tableName").toString();
|
||||
// String bucketName = params.get("bucketName").toString();
|
||||
// String id = params.get("subid").toString();
|
||||
//
|
||||
// // 查询数据
|
||||
// List<Map<String, Object>> maps = dbUtils.loadData("SELECT * FROM " + tableName + " WHERE subid = ?", id);
|
||||
//
|
||||
// // 如果没有数据,直接返回删除成功
|
||||
// if (ObjectUtils.isEmpty(maps)) {
|
||||
// return Result.ok("删除成功");
|
||||
// }
|
||||
//
|
||||
// String mxMapfilename = maps.get(0).get("mx_mapfilename").toString();
|
||||
// String orgId = UserContext.get().getOrgId();
|
||||
// int index = mxMapfilename.indexOf(orgId + "/");
|
||||
//
|
||||
// // 如果没找到,就转换orgId为大写
|
||||
// if (index < 0) {
|
||||
// orgId = orgId.toUpperCase();
|
||||
// index = mxMapfilename.indexOf(orgId + "/");
|
||||
// }
|
||||
//
|
||||
// String objectName = mxMapfilename.substring(index);
|
||||
//
|
||||
// // 删除记录
|
||||
// Map<String, Object> deleteParams = new HashMap<>();
|
||||
// deleteParams.put("subid", id);
|
||||
// int deleteCount = dbUtils.delete(tableName, deleteParams);
|
||||
//
|
||||
// // 如果删除成功,发送删除请求
|
||||
// if (deleteCount > 0) {
|
||||
// if(StringUtils.isBlank(objectName)){
|
||||
// return Result.fail("删除失败,文件格式有误");
|
||||
// }
|
||||
// String requestUrl = String.format("http://g3oss.g3soft.cn:8082/delete?bucketName=%s&objectName=%s", bucketName, objectName);
|
||||
// try {
|
||||
// HttpUtils.get(requestUrl);
|
||||
// } catch (Exception e) {
|
||||
// throw new RuntimeException("删除文件失败",e);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return Result.ok();
|
||||
// }
|
||||
|
||||
@PostMapping("/deleteAliyunFile")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
|
||||
-99
@@ -1,99 +0,0 @@
|
||||
package cn.g3soft.utils;
|
||||
|
||||
import cn.g3soft.config.FileConfig;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通用文件上传工具类
|
||||
* 规则:
|
||||
* - 文件名保持原始或指定
|
||||
* - 同一天同箱号重复 -> 自动生成 _1/_2/... 文件夹
|
||||
* - 使用 FileUtils 拼接路径
|
||||
*/
|
||||
@Component
|
||||
public class FileContainerUtils {
|
||||
|
||||
@Resource
|
||||
private FileConfig fileConfig;
|
||||
|
||||
/**
|
||||
* 单文件上传
|
||||
*/
|
||||
public String upload(MultipartFile file, String containerNo, String fileName) throws IOException {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new RuntimeException("文件不能为空");
|
||||
}
|
||||
|
||||
// 1. 日期路径 yyyy/MM/dd
|
||||
String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
|
||||
|
||||
// 2. 基础目录:上传目录 + 日期
|
||||
String basePath = PathUtils.joinFilePath(fileConfig.getContainerPath(), datePath);
|
||||
|
||||
// 3. 计算最终目录(箱号 + 重复批次)
|
||||
File finalDir = resolveContainerDir(basePath, containerNo);
|
||||
|
||||
if (!finalDir.exists()) {
|
||||
finalDir.mkdirs();
|
||||
}
|
||||
|
||||
// 4. 最终文件
|
||||
File dest = new File(finalDir, fileName);
|
||||
file.transferTo(dest);
|
||||
|
||||
// 5. 相对路径,用于生成URL
|
||||
String relativePath = PathUtils.joinFilePath(
|
||||
datePath,
|
||||
finalDir.getName(),
|
||||
fileName
|
||||
);
|
||||
|
||||
return fileConfig.buildFileUrl(relativePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多文件上传
|
||||
*/
|
||||
public List<String> uploadBatch(MultipartFile[] files, String containerNo, String[] fileNames) throws IOException {
|
||||
if (files == null || files.length == 0) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<String> urls = new ArrayList<>();
|
||||
for (int i = 0; i < files.length; i++) {
|
||||
String name = (fileNames != null && i < fileNames.length) ? fileNames[i] : files[i].getOriginalFilename();
|
||||
urls.add(upload(files[i], containerNo, name));
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析容器目录
|
||||
* - 如果同一天已存在该箱号目录 -> 自动加 _1/_2/... 避免覆盖
|
||||
*/
|
||||
private File resolveContainerDir(String basePath, String containerNo) {
|
||||
File containerDir = new File(PathUtils.joinFilePath(basePath, containerNo));
|
||||
|
||||
if (!containerDir.exists()) {
|
||||
return containerDir;
|
||||
}
|
||||
|
||||
// 已存在 -> 找下一个可用序号
|
||||
int i = 1;
|
||||
while (true) {
|
||||
File dir = new File(PathUtils.joinFilePath(basePath, containerNo + "_" + i));
|
||||
if (!dir.exists()) {
|
||||
return dir;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package cn.g3soft.utils;
|
||||
|
||||
import cn.g3soft.config.FileConfig;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.UUID;
|
||||
|
||||
@Component
|
||||
public class FileUploadUtils {
|
||||
|
||||
@Resource
|
||||
private FileConfig fileConfig;
|
||||
|
||||
/**
|
||||
* 上传文件到临时路径
|
||||
* @param file 上传的文件
|
||||
* @param fileName 如果前端传递了文件名,使用传递的文件名
|
||||
* @return 临时文件的URL
|
||||
*/
|
||||
public String uploadTemp(MultipartFile file, String fileName) throws IOException {
|
||||
// 1. 生成最终文件名
|
||||
String finalFileName = generateTempFileName(file, fileName);
|
||||
|
||||
// 2. 临时目录路径
|
||||
File tempDir = new File(fileConfig.getTempPath());
|
||||
|
||||
// 如果临时目录不存在,创建目录
|
||||
if (!tempDir.exists()) {
|
||||
tempDir.mkdirs();
|
||||
}
|
||||
|
||||
// 3. 保存文件到临时目录
|
||||
File tempFile = new File(tempDir, finalFileName);
|
||||
file.transferTo(tempFile);
|
||||
|
||||
// 4. 返回临时文件的 URL(供 OCR 使用)
|
||||
return fileConfig.buildTempUrl(finalFileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件名生成临时文件名(带时间戳 + UUID + 文件后缀)
|
||||
* @param file 上传的文件
|
||||
* @param fileName 传递的文件名(可选)
|
||||
* @return 生成的最终文件名
|
||||
*/
|
||||
private String generateTempFileName(MultipartFile file, String fileName) {
|
||||
// 优先使用前端传递的 fileName
|
||||
String originalFilename = (fileName != null && !fileName.isEmpty())
|
||||
? fileName
|
||||
: file.getOriginalFilename();
|
||||
|
||||
// 如果没有获取到文件名,设置一个默认文件名
|
||||
if (originalFilename == null) {
|
||||
originalFilename = "file";
|
||||
}
|
||||
|
||||
// 获取文件后缀
|
||||
String extension = "";
|
||||
if (originalFilename.contains(".")) {
|
||||
extension = originalFilename.substring(originalFilename.lastIndexOf("."));
|
||||
}
|
||||
|
||||
// 生成带时间戳和 UUID 的文件名
|
||||
return LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "_"
|
||||
+ UUID.randomUUID().toString().replace("-", "") + extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除临时文件(成功识别后删除)
|
||||
* @param tempFileName 临时文件的文件名
|
||||
*/
|
||||
public void deleteTempFile(String tempFileName) {
|
||||
String filePath = PathUtils.joinFilePath(fileConfig.getBaseDir(), fileConfig.getTempDir(), tempFileName);
|
||||
|
||||
File tempFile = new File(tempFileName);
|
||||
if (tempFile.exists()) {
|
||||
tempFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -114,4 +114,63 @@ public class FileUtils {
|
||||
throw new RuntimeException("上传文件不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static boolean deleteFileByPath(String fullPath) {
|
||||
if (fullPath == null || fullPath.trim().isEmpty()) {
|
||||
throw new RuntimeException("路径不能为空");
|
||||
}
|
||||
|
||||
File file = new File(fullPath);
|
||||
|
||||
// 防止误删
|
||||
if (!file.exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file.isFile()) {
|
||||
throw new RuntimeException("不是文件: " + fullPath);
|
||||
}
|
||||
|
||||
return file.delete();
|
||||
}
|
||||
|
||||
public static boolean deleteDirectory(String dirPath) {
|
||||
if (dirPath == null || dirPath.trim().isEmpty()) {
|
||||
throw new RuntimeException("路径不能为空");
|
||||
}
|
||||
|
||||
File dir = new File(dirPath);
|
||||
|
||||
if (!dir.exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!dir.isDirectory()) {
|
||||
throw new RuntimeException("不是文件夹: " + dirPath);
|
||||
}
|
||||
|
||||
return deleteDirRecursively(dir);
|
||||
}
|
||||
|
||||
private static boolean deleteDirRecursively(File dir) {
|
||||
File[] files = dir.listFiles();
|
||||
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
// 递归删除子目录
|
||||
deleteDirRecursively(file);
|
||||
} else {
|
||||
// 删除文件
|
||||
if (!file.delete()) {
|
||||
throw new RuntimeException("文件删除失败: " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除当前目录
|
||||
return dir.delete();
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,9 @@ public class PathUtils {
|
||||
|
||||
part = part.trim();
|
||||
|
||||
// 替换 Windows 路径分隔符为 URL 的 '/'
|
||||
part = part.replace("\\", "/");
|
||||
|
||||
if (sb.length() == 0) {
|
||||
// 第一段:保留协议(http://)
|
||||
sb.append(part.replaceAll("/+$", ""));
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
VITE_APP_PORT = 8080
|
||||
VITE_APP_PORT = 8082
|
||||
|
||||
VITE_ALIYUN_BUCKET = 'g3hd'
|
||||
@@ -0,0 +1,468 @@
|
||||
<script lang="ts" setup>
|
||||
import type { UploadFile, UploadFileItem } from 'sard-uniapp'
|
||||
import { dialog, isImageUrl, isVideoUrl, toast } from 'sard-uniapp'
|
||||
import { getOrgBaseUrl } from '@/request'
|
||||
import { deleteFileApi, loadDataApi, saveObjectApi, uploadContainerFileApi } from '@/request/api'
|
||||
import { useUserStore } from '@/store'
|
||||
import { buildPubicUrl, downloadFile, getFileExtension, imageCompress, isImage } from '@/utils/file'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const form = ref<any>({
|
||||
b_id: 0,
|
||||
b_container_no: '',
|
||||
b_container_img: '',
|
||||
})
|
||||
|
||||
const subFileList = ref([])
|
||||
|
||||
onLoad((options) => {
|
||||
const id = options.id
|
||||
|
||||
if (id) {
|
||||
form.value.b_id = id
|
||||
|
||||
loadMainData()
|
||||
}
|
||||
})
|
||||
|
||||
async function loadMainData() {
|
||||
toast.loading('正在初始化中...', {
|
||||
overlay: true,
|
||||
})
|
||||
|
||||
try {
|
||||
const orgUrl = await getOrgBaseUrl(userStore.userInfo.orgid)
|
||||
|
||||
// 并发请求(提升性能)
|
||||
const [res, fileRes] = await Promise.all([
|
||||
loadDataApi({
|
||||
viewName: 'v_container',
|
||||
searchCondition: `b_id = '${form.value.b_id}'`,
|
||||
}),
|
||||
loadDataApi({
|
||||
viewName: 'bf_file',
|
||||
searchCondition: `father = '${form.value.b_id}' AND mx_cate_id = '1'`,
|
||||
}),
|
||||
])
|
||||
|
||||
// ================= 主表 =================
|
||||
if (res.success && Array.isArray(res.data) && res.data.length > 0) {
|
||||
const mainData = res.data[0]
|
||||
|
||||
if (mainData?.b_container_img) {
|
||||
mainData.b_container_img = buildPubicUrl(
|
||||
orgUrl,
|
||||
mainData.b_container_img,
|
||||
)
|
||||
}
|
||||
|
||||
form.value = mainData
|
||||
}
|
||||
else {
|
||||
console.warn('未获取到主数据')
|
||||
}
|
||||
|
||||
// ================= 子表 =================
|
||||
if (fileRes.success) {
|
||||
const files = (fileRes.data || []).map(file => ({
|
||||
...file,
|
||||
mx_mapfilename: buildPubicUrl(orgUrl, file.mx_mapfilename),
|
||||
name: file.mx_filename,
|
||||
url: buildPubicUrl(orgUrl, file.mx_mapfilename),
|
||||
}))
|
||||
|
||||
subFileList.value = files
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error('加载失败:', err)
|
||||
}
|
||||
finally {
|
||||
toast.hide()
|
||||
}
|
||||
}
|
||||
|
||||
function onItemClick(fileItem: UploadFileItem) {
|
||||
console.log(fileItem)
|
||||
if (!isImageUrl(fileItem.mx_mapfilename) && !isVideoUrl(fileItem.mx_mapfilename)) {
|
||||
downloadFile(fileItem.mx_mapfilename, fileItem.name)
|
||||
}
|
||||
}
|
||||
|
||||
function beforeRead(file: UploadFile): boolean | Promise<UploadFile> {
|
||||
// 非图片直接允许上传
|
||||
if (!isImage(file.name))
|
||||
return true
|
||||
|
||||
// 图片异步压缩,返回修改后的 UploadFile 对象
|
||||
return imageCompress(file.path).then((res) => {
|
||||
file.path = res.path
|
||||
file.size = res.size
|
||||
return file
|
||||
}).catch((err) => {
|
||||
console.error('图片压缩失败:', err)
|
||||
return file
|
||||
})
|
||||
}
|
||||
|
||||
function beforeRemove(_index: number, _fileItem: UploadFileItem): boolean | Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
dialog.confirm({
|
||||
title: '提示',
|
||||
message: `确定删除该张图片吗?`,
|
||||
onConfirm: () => resolve(),
|
||||
onCancel: () => reject(),
|
||||
onClose: () => reject(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function onRemove(index: number, item: UploadFileItem) {
|
||||
if (item.subid) {
|
||||
deleteFileApi(item.subid).then((res) => {
|
||||
if (res.success) {
|
||||
toast.success('删除成功')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfirm() {
|
||||
if (form.value.b_container_img === '') {
|
||||
toast.fail('集装箱号图片不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (form.value.b_container_no === '') {
|
||||
toast.fail('集装箱号不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (!validateContainerNo(form.value.b_container_no)) {
|
||||
toast.fail('集装箱号不符合规则,请检查')
|
||||
return
|
||||
}
|
||||
|
||||
toast.loading('正在保存中 . . .', {
|
||||
overlay: true,
|
||||
transparent: false,
|
||||
})
|
||||
|
||||
const fileInserts = []
|
||||
|
||||
// 子表
|
||||
for (let index = 0; index < subFileList.value.length; index++) {
|
||||
const fileItem = subFileList.value[index]
|
||||
|
||||
if (fileItem.subid) {
|
||||
// 已上传的文件,无需重复上传
|
||||
continue
|
||||
}
|
||||
|
||||
const res = await uploadContainerFileApi(fileItem.file.path, {
|
||||
jobId: form.value.b_id,
|
||||
containerNo: form.value.b_container_no,
|
||||
fileName: fileItem.name,
|
||||
})
|
||||
|
||||
const maxIndexNumber = Math.max(
|
||||
0,
|
||||
...subFileList.value.map(item => item.mx_xh || 0),
|
||||
)
|
||||
|
||||
if (res.success) {
|
||||
const indexNumber = maxIndexNumber + 1
|
||||
|
||||
fileInserts.push({
|
||||
subid: res.data.uid,
|
||||
father: form.value.b_id,
|
||||
mx_xh: indexNumber,
|
||||
mx_filename: res.data.fileName,
|
||||
mx_filesize: fileItem.file.size,
|
||||
mx_fileext: getFileExtension(fileItem.file.name),
|
||||
mx_mapfilename: res.data.path,
|
||||
mx_moduleid: 'b_container',
|
||||
mx_cate_id: '1',
|
||||
})
|
||||
|
||||
fileItem.subid = res.data.uid
|
||||
}
|
||||
}
|
||||
|
||||
if (fileInserts.length === 0) {
|
||||
toast.success('保存成功')
|
||||
return
|
||||
}
|
||||
|
||||
const reqData = [
|
||||
{
|
||||
tableName: 'bf_file',
|
||||
keyField: 'subid',
|
||||
inserts: fileInserts,
|
||||
updates: [],
|
||||
deletes: [],
|
||||
},
|
||||
]
|
||||
|
||||
saveObjectApi(reqData).then((res) => {
|
||||
if (res.success) {
|
||||
toast.success('保存成功')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 集装箱号校验
|
||||
function validateContainerNo(containerNo) {
|
||||
// 正则表达式: 4个字母 + 7个数字
|
||||
const pattern = /^[A-Z]{4}\d{7}$/i
|
||||
|
||||
// 使用正则进行匹配
|
||||
if (pattern.test(containerNo)) {
|
||||
return true
|
||||
}
|
||||
else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="g3-container">
|
||||
<view class="g3-content">
|
||||
<wd-cell-group custom-class="group" title="集装箱号(OCR识别)" border use-slot>
|
||||
<template #title>
|
||||
<view style="display:flex;align-items: center;">
|
||||
<sar-icon family="iconfont" name="iconyuanbanben_OCR" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
|
||||
<text>
|
||||
箱门照(OCR识别)
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<view class="step-content box">
|
||||
<view class="box-main">
|
||||
<view class="box-image">
|
||||
<wd-img
|
||||
:src="form.b_container_img"
|
||||
width="100%"
|
||||
height="100%"
|
||||
:enable-preview="true"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="box-input">
|
||||
<view class="header">
|
||||
<view class="label-container">
|
||||
<view>集装箱号</view>
|
||||
<view class="required-asterisk">
|
||||
*
|
||||
</view>
|
||||
</view>
|
||||
<view class="format">
|
||||
格式: 4字母 + 7数字
|
||||
</view>
|
||||
</view>
|
||||
<sar-input
|
||||
v-model="form.b_container_no"
|
||||
disabled
|
||||
placeholder="请输入箱号"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</wd-cell-group>
|
||||
|
||||
<wd-cell-group custom-class="group" border use-slot style="margin-top: 32rpx;">
|
||||
<template #title>
|
||||
<view style="display:flex;align-items: center;">
|
||||
<sar-icon family="iconfont" name="tupian" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
|
||||
<text>
|
||||
其他图片
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<view class="step-content">
|
||||
<sar-upload
|
||||
v-model="subFileList"
|
||||
multiple
|
||||
:before-read="beforeRead"
|
||||
:before-remove="beforeRemove"
|
||||
@item-click="onItemClick"
|
||||
@remove="onRemove"
|
||||
/>
|
||||
</view>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
<view class="g3-footer">
|
||||
<view class="footer-main">
|
||||
<sar-button round root-style="width:100%" icon-family="iconfont" icon="fasongtijiao" @click="handleConfirm">
|
||||
保存
|
||||
</sar-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
page {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.g3-result {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.result-tip {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.container-no {
|
||||
font-weight: 600;
|
||||
color: #4caf50;
|
||||
}
|
||||
}
|
||||
|
||||
.g3-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.g3-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16rpx 0rpx;
|
||||
}
|
||||
|
||||
.g3-footer {
|
||||
width: 100%;
|
||||
|
||||
.footer-main {
|
||||
padding: 16rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.step {
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
.step-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.step-content {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.box-main {
|
||||
height: 326rpx;
|
||||
}
|
||||
|
||||
.box-placeholder {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
background: var(--sar-upload-select-bg);
|
||||
border-radius: var(--sar-upload-select-border-radius);
|
||||
|
||||
.placeholder-text {
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.box-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
.delete-btn {
|
||||
position: absolute;
|
||||
top: 12rpx;
|
||||
right: 12rpx;
|
||||
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.box-input {
|
||||
margin-top: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 12rpx;
|
||||
width: 100%;
|
||||
.label-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.required-asterisk {
|
||||
position: absolute;
|
||||
top: -5rpx;
|
||||
right: -12rpx;
|
||||
color: red;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.format {
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.tip {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border-radius: 999px;
|
||||
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #3a6ee8, #5c8df6);
|
||||
box-shadow: 0 6rpx 12rpx rgba(58, 110, 232, 0.18);
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.submit-btn:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 3rpx 6rpx rgba(58, 110, 232, 0.15);
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import type { UploadFile, UploadFileItem } from 'sard-uniapp'
|
||||
import { dialog, isImageUrl, isVideoUrl, toast } from 'sard-uniapp'
|
||||
import { aliyunpluploadApi, deleteAliyunFileApi, deleteFileApi, getUniqueIdApi, recognizeOcrBoxApi, saveObjectApi, uploadTempFileApi } from '@/request/api'
|
||||
import { getUniqueIdApi, recognizeOcrBoxApi, saveObjectApi, uploadContainerFileApi, uploadTempFileApi } from '@/request/api'
|
||||
import { downloadFile, getFileExtension, imageCompress, isImage } from '@/utils/file'
|
||||
import { uploadToOss } from '@/utils/oss'
|
||||
|
||||
const boxImage = ref(null)
|
||||
|
||||
@@ -14,8 +13,8 @@ const showResult = ref(false)
|
||||
|
||||
const form = ref({
|
||||
b_id: 0,
|
||||
containerNo: '',
|
||||
containerImageUrl: '',
|
||||
b_container_no: '',
|
||||
b_container_img: '',
|
||||
ocrScore: 0.00,
|
||||
ocrNo: '',
|
||||
})
|
||||
@@ -53,11 +52,14 @@ function handleChooseBox() {
|
||||
})
|
||||
|
||||
imageCompress(tempFile.path).then((compressRes) => {
|
||||
console.log(compressRes)
|
||||
uploadTempFileApi(compressRes.path, tempFile.name).then((res) => {
|
||||
form.value.b_container_img = compressRes.path
|
||||
|
||||
uploadTempFileApi(compressRes.path, {
|
||||
fileName: tempFile.name,
|
||||
}).then((res) => {
|
||||
if (res.success) {
|
||||
recognizeOcrBoxApi({
|
||||
fileUrl: res.data.tempUrl,
|
||||
fileUrl: res.data.url,
|
||||
}).then((ocrRes) => {
|
||||
if (ocrRes.success) {
|
||||
toast.success('识别成功,请检查箱号是否正确')
|
||||
@@ -65,7 +67,7 @@ function handleChooseBox() {
|
||||
const score = Number(ocrRes?.data?.score ?? 0)
|
||||
form.value.ocrScore = Math.floor(score * 100) / 100
|
||||
|
||||
form.value.containerNo = ocrRes.data.boxNumber
|
||||
form.value.b_container_no = ocrRes.data.boxNumber
|
||||
form.value.ocrNo = ocrRes.data.boxNumber
|
||||
|
||||
isError.value = false
|
||||
@@ -88,29 +90,17 @@ const computedOcrScore = computed(() => {
|
||||
|
||||
function resetForm() {
|
||||
boxImage.value = null
|
||||
form.value.containerImageUrl = ''
|
||||
form.value.containerNo = ''
|
||||
form.value.b_container_no = ''
|
||||
form.value.ocrNo = ''
|
||||
form.value.ocrScore = 0.00
|
||||
|
||||
showResult.value = false
|
||||
isError.value = false
|
||||
ocrFinish.value = false
|
||||
}
|
||||
|
||||
function handleDeleteBoxImage() {
|
||||
if (!form.value.containerImageUrl) {
|
||||
resetForm()
|
||||
return
|
||||
}
|
||||
|
||||
toast.loading('正在删除中 . . .', {
|
||||
overlay: true,
|
||||
transparent: false,
|
||||
})
|
||||
|
||||
deleteAliyunFileApi(form.value.containerImageUrl).then((res) => {
|
||||
if (res.success) {
|
||||
resetForm()
|
||||
toast.success('删除成功')
|
||||
}
|
||||
})
|
||||
resetForm()
|
||||
}
|
||||
|
||||
function onItemClick(fileItem: UploadFileItem) {
|
||||
@@ -136,9 +126,7 @@ function beforeRead(file: UploadFile): boolean | Promise<UploadFile> {
|
||||
})
|
||||
}
|
||||
|
||||
function beforeRemove(index: number, fileItem: UploadFileItem): boolean | Promise<void> {
|
||||
console.log(fileItem)
|
||||
|
||||
function beforeRemove(_index: number, _fileItem: UploadFileItem): boolean | Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
dialog.confirm({
|
||||
title: '提示',
|
||||
@@ -150,28 +138,18 @@ function beforeRemove(index: number, fileItem: UploadFileItem): boolean | Promis
|
||||
})
|
||||
}
|
||||
|
||||
function onRemove(index: number, item: UploadFileItem) {
|
||||
if (item.status === 'done') {
|
||||
deleteAliyunFileApi(item.url).then((res) => {
|
||||
if (res.success) {
|
||||
toast.success('删除成功')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfirm() {
|
||||
if (form.value.containerImageUrl === '') {
|
||||
if (form.value.b_container_img === '') {
|
||||
toast.fail('集装箱号图片不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (form.value.containerNo === '') {
|
||||
if (form.value.b_container_no === '') {
|
||||
toast.fail('集装箱号不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (!validateContainerNo(form.value.containerNo)) {
|
||||
if (!validateContainerNo(form.value.b_container_no)) {
|
||||
toast.fail('集装箱号不符合规则,请检查')
|
||||
return
|
||||
}
|
||||
@@ -181,82 +159,88 @@ async function handleConfirm() {
|
||||
transparent: false,
|
||||
})
|
||||
|
||||
//
|
||||
const mainTable = {
|
||||
b_id: form.value.b_id,
|
||||
b_container_no: form.value.b_container_no,
|
||||
b_container_img: form.value.b_container_img,
|
||||
b_ocr_no: form.value.ocrNo,
|
||||
b_ocr_score: form.value.ocrScore || 0.00,
|
||||
}
|
||||
const fileInserts = []
|
||||
// 主表
|
||||
await uploadContainerFileApi(boxImage.value.path, {
|
||||
jobId: form.value.b_id,
|
||||
containerNo: form.value.b_container_no,
|
||||
fileName: boxImage.value.name,
|
||||
}).then((res) => {
|
||||
if (res.success) {
|
||||
mainTable.b_container_img = res.data.path
|
||||
|
||||
// // 图片已经上传成功,保存主表即可
|
||||
// const mainTable = {
|
||||
// b_id: form.value.b_id,
|
||||
// b_container_no: form.value.containerNo,
|
||||
// b_container_img: form.value.containerImageUrl,
|
||||
// b_ocr_no: form.value.ocrNo,
|
||||
// b_ocr_score: form.value.ocrScore || 0.00,
|
||||
// }
|
||||
fileInserts.push({
|
||||
subid: res.data.uid,
|
||||
father: form.value.b_id,
|
||||
mx_xh: 0,
|
||||
mx_filename: res.data.fileName,
|
||||
mx_filesize: boxImage.value.size,
|
||||
mx_fileext: getFileExtension(boxImage.value.name),
|
||||
mx_mapfilename: res.data.path,
|
||||
mx_moduleid: 'b_container',
|
||||
mx_cate_id: '0',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// const fileInserts = []
|
||||
// 子表
|
||||
for (let index = 0; index < subFileList.value.length; index++) {
|
||||
const fileItem = subFileList.value[index]
|
||||
|
||||
// const containerNo = form.value.containerNo
|
||||
const res = await uploadContainerFileApi(fileItem.file.path, {
|
||||
jobId: form.value.b_id,
|
||||
containerNo: form.value.b_container_no,
|
||||
fileName: fileItem.name,
|
||||
})
|
||||
|
||||
// subFileList.value.forEach(async (fileItem, index) => {
|
||||
// const uniqueRes = await getUniqueIdApi()
|
||||
// const indexNumber = index + 2
|
||||
if (res.success) {
|
||||
const indexNumber = index + 2
|
||||
|
||||
// fileInserts.push({
|
||||
// subid: uniqueRes.data,
|
||||
// father: form.value.b_id,
|
||||
// mx_xh: indexNumber,
|
||||
// mx_filename: buildFileName(containerNo, indexNumber, fileItem.file.name),
|
||||
// mx_filesize: fileItem.file.size,
|
||||
// mx_fileext: getFileExtension(fileItem.file.name),
|
||||
// mx_mapfilename: fileItem.url,
|
||||
// mx_moduleid: 'b_container',
|
||||
// mx_cate_id: '1',
|
||||
// })
|
||||
// })
|
||||
fileInserts.push({
|
||||
subid: res.data.uid,
|
||||
father: form.value.b_id,
|
||||
mx_xh: indexNumber,
|
||||
mx_filename: res.data.fileName,
|
||||
mx_filesize: fileItem.file.size,
|
||||
mx_fileext: getFileExtension(fileItem.file.name),
|
||||
mx_mapfilename: res.data.path,
|
||||
mx_moduleid: 'b_container',
|
||||
mx_cate_id: '1',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// const uniqueRes = await getUniqueIdApi()
|
||||
const reqData = [
|
||||
{
|
||||
tableName: 'b_container',
|
||||
keyField: 'b_id',
|
||||
inserts: [mainTable],
|
||||
updates: [],
|
||||
deletes: [],
|
||||
},
|
||||
{
|
||||
tableName: 'bf_file',
|
||||
keyField: 'subid',
|
||||
inserts: fileInserts,
|
||||
updates: [],
|
||||
deletes: [],
|
||||
},
|
||||
]
|
||||
|
||||
// fileInserts.push({
|
||||
// subid: uniqueRes.data,
|
||||
// father: form.value.b_id,
|
||||
// mx_xh: 0,
|
||||
// mx_filename: buildFileName(containerNo, 1, boxImage.value.name),
|
||||
// mx_filesize: boxImage.value.size,
|
||||
// mx_fileext: getFileExtension(boxImage.value.name),
|
||||
// mx_mapfilename: form.value.containerImageUrl,
|
||||
// mx_moduleid: 'b_container',
|
||||
// mx_cate_id: '0',
|
||||
// })
|
||||
|
||||
// const reqData = [
|
||||
// {
|
||||
// tableName: 'b_container',
|
||||
// keyField: 'b_id',
|
||||
// inserts: [mainTable],
|
||||
// updates: [],
|
||||
// deletes: [],
|
||||
// },
|
||||
// {
|
||||
// tableName: 'bf_file',
|
||||
// keyField: 'subid',
|
||||
// inserts: fileInserts,
|
||||
// updates: [],
|
||||
// deletes: [],
|
||||
// },
|
||||
// ]
|
||||
|
||||
// saveObjectApi(reqData).then((res) => {
|
||||
// if (res.success) {
|
||||
// toast.success('保存成功')
|
||||
// tipContainerNo.value = form.value.containerNo
|
||||
// showResult.value = true
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
function buildFileName(containerNo, index, fileName) {
|
||||
const formattedIndex = String(index).padStart(2, '0')
|
||||
|
||||
return `${containerNo}_${formattedIndex}.${fileName.split('.').pop()}`
|
||||
saveObjectApi(reqData).then((res) => {
|
||||
if (res.success) {
|
||||
toast.success('保存成功')
|
||||
tipContainerNo.value = form.value.b_container_no
|
||||
showResult.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 集装箱号校验
|
||||
@@ -302,7 +286,7 @@ async function continueAdd() {
|
||||
<view style="display:flex;align-items: center;">
|
||||
<sar-icon family="iconfont" name="iconyuanbanben_OCR" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
|
||||
<text>
|
||||
集装箱号(OCR识别)
|
||||
箱门照(OCR识别)
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
@@ -367,7 +351,7 @@ async function continueAdd() {
|
||||
</view>
|
||||
</view>
|
||||
<sar-input
|
||||
v-model="form.containerNo"
|
||||
v-model="form.b_container_no"
|
||||
placeholder="请输入箱号"
|
||||
/>
|
||||
</view>
|
||||
@@ -390,7 +374,6 @@ async function continueAdd() {
|
||||
:before-read="beforeRead"
|
||||
:before-remove="beforeRemove"
|
||||
@item-click="onItemClick"
|
||||
@remove="onRemove"
|
||||
/>
|
||||
</view>
|
||||
</wd-cell-group>
|
||||
|
||||
@@ -95,6 +95,13 @@
|
||||
"navigationBarTitleText": "集装箱装货采集",
|
||||
"navigationStyle": "default"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "upload/edit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "集装箱装货采集(更新)",
|
||||
"navigationStyle": "default"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { toast } from 'sard-uniapp'
|
||||
import { ref } from 'vue'
|
||||
import { getOrgBaseUrl } from '@/request'
|
||||
import { loadPageDataApi } from '@/request/api'
|
||||
import { useUserStore } from '@/store'
|
||||
import { deepEqual, groupFieldsBySpan } from '@/utils/common'
|
||||
import { getCurrentDate } from '@/utils/time'
|
||||
import { groupFieldsBySpan } from '@/utils/common'
|
||||
import { buildPubicUrl } from '@/utils/file'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const pagingRef = ref<any>(null)
|
||||
const dataList = ref<any[]>([])
|
||||
|
||||
const itemFields = [
|
||||
{ label: '录入时间', value: item => item.b_inputdatetime, span: 12 },
|
||||
{ label: '文件数量', value: item => item.v_file_count, span: 12 },
|
||||
]
|
||||
const groupedFields = groupFieldsBySpan(itemFields)
|
||||
|
||||
// 搜索关键字
|
||||
const searchValue = ref('')
|
||||
|
||||
@@ -34,15 +29,18 @@ function handleSearch() {
|
||||
}
|
||||
|
||||
// 模拟分页请求
|
||||
function queryList(pageNo: number, pageSize: number) {
|
||||
async function queryList(pageNo: number, pageSize: number) {
|
||||
toast.loading('正在加载中')
|
||||
|
||||
const orgid = userStore.userInfo.orgid
|
||||
const userid = userStore.userInfo.userid
|
||||
|
||||
const orgUrl = await getOrgBaseUrl(orgid)
|
||||
|
||||
let searchCondition = `2 > 1 AND b_inputuser_id = '${userid}'`
|
||||
|
||||
if (searchValue.value) {
|
||||
searchCondition += `b_container_no LIKE '%${searchValue.value}%' `
|
||||
searchCondition += `AND b_container_no LIKE '%${searchValue.value}%' `
|
||||
}
|
||||
|
||||
loadPageDataApi({
|
||||
@@ -54,8 +52,11 @@ function queryList(pageNo: number, pageSize: number) {
|
||||
})
|
||||
.then((res) => {
|
||||
const rows = res.data.rows || []
|
||||
console.log(rows)
|
||||
|
||||
if (rows) {
|
||||
rows.forEach((item) => {
|
||||
item.b_container_img = buildPubicUrl(orgUrl, item.b_container_img)
|
||||
})
|
||||
pagingRef.value.complete(rows)
|
||||
}
|
||||
})
|
||||
@@ -64,12 +65,10 @@ function queryList(pageNo: number, pageSize: number) {
|
||||
})
|
||||
}
|
||||
|
||||
function handleToFile(itemData) {
|
||||
const searchids = [itemData.b_id, itemData.father]
|
||||
.filter(Boolean)
|
||||
.join(',')
|
||||
function handleToDetail(item) {
|
||||
// 跳转到详情页
|
||||
uni.navigateTo({
|
||||
url: `/pages-sub/file/index?moduleid=xc_conyard_in&father=${itemData.father}&searchids=${encodeURIComponent(searchids)}&cateid=016&edit=1`,
|
||||
url: `/pages-sub/upload/edit?id=${item.b_id}`,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -96,13 +95,13 @@ function handleToFile(itemData) {
|
||||
v-for="(item, index) in dataList"
|
||||
:key="index"
|
||||
class="list-item"
|
||||
@click="handleToDetail(item)"
|
||||
>
|
||||
<view class="list-item__header">
|
||||
<view class="list-item__main">
|
||||
<sar-image
|
||||
:src="item.b_container_img"
|
||||
width="160rpx"
|
||||
height="160rpx"
|
||||
<wd-img
|
||||
width="160rpx" height="160rpx" :src="item.b_container_img" :enable-preview="true"
|
||||
@click.stop
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
@@ -195,6 +194,12 @@ page {
|
||||
position: relative;
|
||||
padding: 16rpx;
|
||||
background-color: #fff;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:active {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
&.list-item--desktop {
|
||||
.list-item__header {
|
||||
max-width: 1000rpx;
|
||||
|
||||
@@ -45,8 +45,6 @@ export function recognizeOcrBoxApi(params: OcrBoxRequest) {
|
||||
export function deleteFileApi(subid) {
|
||||
return httpPost(`file/delete`, {
|
||||
subid,
|
||||
tableName: 'bf_file',
|
||||
bucketName: aliyunBucket,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -76,8 +74,8 @@ export function getUniqueIdApi() {
|
||||
interface LoadDataParams {
|
||||
viewName: string
|
||||
searchCondition: string
|
||||
orderField: string
|
||||
searchColumns: string[]
|
||||
orderField?: string
|
||||
searchColumns?: string[]
|
||||
}
|
||||
|
||||
export function loadDataApi(params: LoadDataParams) {
|
||||
@@ -109,6 +107,28 @@ export function loadPageDataApi(params: PageParams) {
|
||||
)
|
||||
}
|
||||
|
||||
export function uploadTempFileApi(filePath: string, fileName: string) {
|
||||
return httpUpload('/file/uploadTemp', filePath, fileName)
|
||||
interface UpdateTempFileParams {
|
||||
fileName: string
|
||||
}
|
||||
|
||||
export function uploadTempFileApi(filePath: string, params: UpdateTempFileParams) {
|
||||
return httpUpload<{
|
||||
url: string
|
||||
path: string
|
||||
}>('/file/uploadTemp', filePath, params)
|
||||
}
|
||||
|
||||
interface UpdateContainerFileParams {
|
||||
jobId: string | number
|
||||
containerNo: string
|
||||
fileName: string
|
||||
}
|
||||
|
||||
export function uploadContainerFileApi(filePath: string, params: UpdateContainerFileParams) {
|
||||
return httpUpload<{
|
||||
url: string
|
||||
path: string
|
||||
uid: number
|
||||
fileName: string
|
||||
}>('/file/uploadContainer', filePath, params)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { toast } from 'sard-uniapp'
|
||||
import * as dbJson from '@/static/db.json'
|
||||
import { useUserStore } from '@/store'
|
||||
import { joinUrl } from '@/utils/common'
|
||||
|
||||
import { isH5 } from '@/utils/platform'
|
||||
|
||||
/**
|
||||
@@ -27,6 +28,48 @@ function readDbJson(): Promise<Record<string, string>> {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据机构码获取原始 baseUrl(不拼接)
|
||||
* @param orgid 机构码
|
||||
* @returns Promise<string>
|
||||
*/
|
||||
export async function getOrgBaseUrl(orgid: string): Promise<string> {
|
||||
if (!orgid) {
|
||||
return ''
|
||||
}
|
||||
|
||||
let dbRes: Record<string, string> = {}
|
||||
|
||||
// 读取配置(和你原逻辑保持一致)
|
||||
if (isH5) {
|
||||
const timestamp = Date.now()
|
||||
dbRes = await new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `/g3ocr-app/static/db.json?t=${timestamp}`,
|
||||
method: 'GET',
|
||||
success: res => resolve(res.data as Record<string, string>),
|
||||
fail: err => reject(err),
|
||||
})
|
||||
})
|
||||
}
|
||||
else {
|
||||
dbRes = dbJson as Record<string, string>
|
||||
}
|
||||
|
||||
if (!dbRes || Object.keys(dbRes).length === 0) {
|
||||
throw new Error('机构配置获取失败')
|
||||
}
|
||||
|
||||
const baseUrl = dbRes[orgid]
|
||||
|
||||
if (!baseUrl) {
|
||||
throw new Error('机构码无效')
|
||||
}
|
||||
|
||||
// 👉 关键:只返回原始 JSON 里的值
|
||||
return baseUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* ======================
|
||||
* 获取 baseUrl + headers
|
||||
@@ -200,7 +243,6 @@ function request<T = any>(
|
||||
export function httpUpload<T = any>(
|
||||
url: string,
|
||||
filePath: string,
|
||||
fileName: string,
|
||||
formData?: Record<string, any>,
|
||||
option?: UniApp.UploadFileOption,
|
||||
) {
|
||||
@@ -217,17 +259,12 @@ export function httpUpload<T = any>(
|
||||
|
||||
const fullUrl = joinUrl(baseUrl, url)
|
||||
|
||||
const mergedFormData = {
|
||||
...formData,
|
||||
fileName,
|
||||
}
|
||||
|
||||
return new Promise<IResData<T>>((resolve, reject) => {
|
||||
task = uni.uploadFile({
|
||||
url: fullUrl,
|
||||
filePath,
|
||||
name: 'file',
|
||||
formData: mergedFormData,
|
||||
formData,
|
||||
header: {
|
||||
...headers,
|
||||
...(option?.header || {}),
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { toast } from 'sard-uniapp'
|
||||
import { getOrgBaseUrl } from '@/request'
|
||||
import { useUserStore } from '@/store'
|
||||
import { getOssKey } from './oss'
|
||||
import { isApp, isH5, isMpWeixin } from './platform'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const aliyunBucket = import.meta.env.VITE_ALIYUN_BUCKET
|
||||
|
||||
/**
|
||||
@@ -115,8 +118,6 @@ export function formatFileSize(size: number): string {
|
||||
else return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 上传文件到阿里云 OSS(优化版)
|
||||
*/
|
||||
@@ -359,3 +360,39 @@ function downloadAppMedia(fileUrl: string, type: 'image' | 'video') {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function buildPubicUrl(orgUrl, path: string) {
|
||||
if (!path)
|
||||
return ''
|
||||
|
||||
return joinUrl(orgUrl, '/static/file', path)
|
||||
}
|
||||
|
||||
export function joinUrl(...parts: (string | undefined | null)[]): string {
|
||||
if (!parts || parts.length === 0)
|
||||
return ''
|
||||
|
||||
const sb: string[] = []
|
||||
|
||||
for (let part of parts) {
|
||||
if (!part || !part.trim())
|
||||
continue
|
||||
|
||||
part = part.trim()
|
||||
|
||||
// 替换 Windows 路径分隔符
|
||||
part = part.replace(/\\/g, '/')
|
||||
|
||||
if (sb.length === 0) {
|
||||
// 第一段:保留协议,去掉末尾 /
|
||||
sb.push(part.replace(/\/+$/, ''))
|
||||
}
|
||||
else {
|
||||
// 后续段:去掉前后 /
|
||||
sb.push(part.replace(/^\/+|\/+$/g, ''))
|
||||
}
|
||||
}
|
||||
|
||||
// 中间用 / 拼接
|
||||
return sb.join('/')
|
||||
}
|
||||
@@ -10,6 +10,7 @@ PUBLIC_SERVER_PORT=8080
|
||||
|
||||
# API
|
||||
# 后端 API 基础地址
|
||||
PUBLIC_API_BASE_URL=http://ser369.g3soft.cn:80/api
|
||||
# PUBLIC_API_BASE_URL=http://ser369.g3soft.cn:80/api
|
||||
PUBLIC_API_BASE_URL=http://192.168.1.137:80/api
|
||||
|
||||
PUBLIC_ALIYUN_BUCKET = g3hd
|
||||
+1
@@ -39,6 +39,7 @@ declare module 'vue' {
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
G3Column: typeof import('./src/components/g3-table/g3-column.vue')['default']
|
||||
G3ContainerFile: typeof import('./src/components/g3-container-file/index.vue')['default']
|
||||
G3Detail: typeof import('./src/components/g3-detail/index.vue')['default']
|
||||
G3Error: typeof import('./src/components/g3-error/index.vue')['default']
|
||||
G3File: typeof import('./src/components/g3-file/index.vue')['default']
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -27,8 +27,15 @@ import {
|
||||
getAliyunFileBlob,
|
||||
getFileExtension,
|
||||
uploadFile,
|
||||
buildPublicUrl,
|
||||
} from "@/utils/file";
|
||||
import { loadDataApi, saveObjectKeysApi, deleteFileApi } from "@/request/api";
|
||||
import {
|
||||
loadDataApi,
|
||||
insertFileApi,
|
||||
saveObjectApi,
|
||||
deleteFileApi,
|
||||
uploadCommonFileApi,
|
||||
} from "@/request/api";
|
||||
|
||||
const props = defineProps({
|
||||
father: {
|
||||
@@ -158,7 +165,7 @@ async function handleConfirmUpdate() {
|
||||
];
|
||||
|
||||
try {
|
||||
const response = await saveObjectKeysApi(requestData);
|
||||
const response = await saveObjectApi(requestData);
|
||||
|
||||
if (response.success) {
|
||||
ElMessage.success("文件名更新成功");
|
||||
@@ -239,8 +246,15 @@ async function loadFiles() {
|
||||
const res = await loadDataApi("bf_file", searchCondition, "mx_xh");
|
||||
|
||||
if (res.success) {
|
||||
fileList.value = res.data;
|
||||
oldFileList.value = JSON.parse(JSON.stringify(res.data));
|
||||
let datas = res.data.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
mx_mapfilename: buildPublicUrl(item.mx_mapfilename),
|
||||
};
|
||||
});
|
||||
|
||||
fileList.value = datas;
|
||||
oldFileList.value = JSON.parse(JSON.stringify(datas));
|
||||
loading.value = false;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -588,45 +602,69 @@ async function deleteFiles(files) {
|
||||
});
|
||||
}
|
||||
|
||||
// 保存
|
||||
// 保存
|
||||
async function handleSave() {
|
||||
loading.value = true;
|
||||
loadingTip.value = "正在保存中 . . .";
|
||||
|
||||
const { insertdata, updatedata, deletedata, hasChanges } = compareAndClassifyData(
|
||||
fileList.value,
|
||||
oldFileList.value,
|
||||
"subid",
|
||||
["_content", "_status", "checked"],
|
||||
);
|
||||
try {
|
||||
const { insertdata, updatedata, deletedata, hasChanges } = compareAndClassifyData(
|
||||
fileList.value,
|
||||
oldFileList.value,
|
||||
"subid",
|
||||
["_content", "_status", "checked"],
|
||||
);
|
||||
|
||||
// 处理新增的文件
|
||||
for (const item of insertdata) {
|
||||
try {
|
||||
await uploadFile(
|
||||
item._content,
|
||||
item.mx_filename,
|
||||
item.mx_fileext,
|
||||
item.mx_filesize,
|
||||
item.mx_xh,
|
||||
props.moduleId,
|
||||
props.father,
|
||||
);
|
||||
console.log(`文件 ${item.mx_filename} 上传成功`);
|
||||
item._status = "";
|
||||
} catch (error) {
|
||||
console.error(`文件 ${item.mx_filename} 上传失败:`, error);
|
||||
ElMessage.error(`文件 ${item.mx_filename} 上传失败,请重试`);
|
||||
// 没有变化直接结束
|
||||
if (!hasChanges) {
|
||||
ElMessage.info("没有数据变更");
|
||||
return;
|
||||
}
|
||||
|
||||
const mxXh =
|
||||
fileList.value.length > 0 ? Math.max(...fileList.value.map((item) => item.mx_xh ?? 0)) : 2;
|
||||
|
||||
// 处理新增
|
||||
for (const item of insertdata) {
|
||||
try {
|
||||
const uploadRes = await uploadCommonFileApi(item.mx_mapfilename, {
|
||||
fileName: item.mx_filename,
|
||||
});
|
||||
|
||||
if (!uploadRes.success) {
|
||||
throw new Error("上传失败");
|
||||
}
|
||||
|
||||
const insertRes = await insertFileApi({
|
||||
father: props.father,
|
||||
mx_xh: mxXh + 1,
|
||||
mx_filename: item.mx_filename,
|
||||
mx_filesize: item.mx_filesize,
|
||||
mx_fileext: item.mx_fileext,
|
||||
mx_mapfilename: uploadRes.data.path,
|
||||
mx_moduleid: props.moduleId,
|
||||
mx_cate_id: props.cateId,
|
||||
});
|
||||
|
||||
if (!insertRes.success) {
|
||||
throw new Error("入库失败");
|
||||
}
|
||||
|
||||
console.log(`文件 ${item.mx_filename} 上传成功`);
|
||||
item._status = "";
|
||||
} catch (error) {
|
||||
console.error(`文件 ${item.mx_filename} 处理失败:`, error);
|
||||
ElMessage.error(`文件 ${item.mx_filename} 上传失败,请重试`);
|
||||
}
|
||||
}
|
||||
|
||||
ElMessage.success("保存成功");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loadFiles();
|
||||
}
|
||||
|
||||
ElMessage.success("上传成功");
|
||||
|
||||
loading.value = false;
|
||||
|
||||
loadFiles();
|
||||
}
|
||||
|
||||
async function handleDownloadZip() {
|
||||
if (checkedFiles.value.length < 2) {
|
||||
ElMessage.warning("打包文件数量不得少于2");
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { httpPost, httpUpload } from ".";
|
||||
|
||||
|
||||
|
||||
// 登录
|
||||
export function loginApi(orgid, userid, password) {
|
||||
return httpPost(`/auth/login`, {
|
||||
@@ -190,11 +188,31 @@ export function insertFileApi(params) {
|
||||
return httpPost("/file/insert", params);
|
||||
}
|
||||
|
||||
// export function deleteFileApi(subid) {
|
||||
// return httpPost("/file/delete", {
|
||||
// subid,
|
||||
// tableName: "bf_file",
|
||||
// bucketName: import.meta.env.PUBLIC_ALIYUN_BUCKET,
|
||||
// });
|
||||
// }
|
||||
|
||||
export function deleteFileApi(subid) {
|
||||
return httpPost("/file/delete", {
|
||||
subid,
|
||||
tableName: "bf_file",
|
||||
bucketName: import.meta.env.PUBLIC_ALIYUN_BUCKET,
|
||||
});
|
||||
}
|
||||
|
||||
export function uploadContainerFileApi(filePath, { jobId, containerNo, fileName }) {
|
||||
return httpUpload("/file/uploadContainer", filePath, {
|
||||
jobId,
|
||||
containerNo,
|
||||
fileName,
|
||||
});
|
||||
}
|
||||
|
||||
export function uploadCommonFileApi(filePath, { fileName }) {
|
||||
return httpUpload("/file/uploadCommon", filePath, {
|
||||
fileName,
|
||||
});
|
||||
}
|
||||
// ============================= file =============================
|
||||
@@ -120,19 +120,17 @@ export function httpPost(url, data, config) {
|
||||
* @param {Object} config - axios 配置(可选)
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export function httpUpload(url, file, extraData = {}, config = {}) {
|
||||
export async function httpUpload(url, file, extraData = {}, config = {}) {
|
||||
const formData = new FormData();
|
||||
|
||||
// 兼容单文件 & 多文件
|
||||
if (Array.isArray(file)) {
|
||||
file.forEach((f) => {
|
||||
formData.append("file", f);
|
||||
});
|
||||
for (const f of file) {
|
||||
await appendFile(formData, f);
|
||||
}
|
||||
} else {
|
||||
formData.append("file", file);
|
||||
await appendFile(formData, file);
|
||||
}
|
||||
|
||||
// 追加额外参数
|
||||
Object.keys(extraData).forEach((key) => {
|
||||
formData.append(key, extraData[key]);
|
||||
});
|
||||
@@ -147,6 +145,24 @@ export function httpUpload(url, file, extraData = {}, config = {}) {
|
||||
.then((res) => res.data);
|
||||
}
|
||||
|
||||
async function appendFile(formData, file) {
|
||||
// ✅ 已经是 File / Blob
|
||||
if (file instanceof File || file instanceof Blob) {
|
||||
formData.append("file", file);
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ 处理 blob:http://xxx
|
||||
if (typeof file === "string" && file.startsWith("blob:")) {
|
||||
const blob = await fetch(file).then((res) => res.blob());
|
||||
|
||||
formData.append("file", blob);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("不支持的文件类型:" + typeof file);
|
||||
}
|
||||
|
||||
export function reset401Flag() {
|
||||
hasShown401 = false;
|
||||
}
|
||||
|
||||
@@ -203,7 +203,8 @@ export function getFileExtension(fileName) {
|
||||
return fileExt;
|
||||
}
|
||||
|
||||
export function downloadZip(files, fileName = "文件合集", callback) {
|
||||
|
||||
export function downloadOssZip(files, fileName = "文件合集", callback) {
|
||||
const zip = new JSZip();
|
||||
const addedFiles = new Set();
|
||||
|
||||
@@ -415,3 +416,78 @@ export function getAliyunFileBlob(url) {
|
||||
}
|
||||
|
||||
// ============================================ 阿里云上传和下载 ============================================
|
||||
|
||||
// ============================================ 本地文件上传和下载 ==========================================
|
||||
export function buildPublicUrl(filePath) {
|
||||
const baseUrl = import.meta.env.PUBLIC_API_BASE_URL || "";
|
||||
|
||||
return joinUrl(baseUrl, "/static/file", filePath);
|
||||
}
|
||||
|
||||
export function getFileBlob(path) {
|
||||
const fullUrl = buildPublicUrl(path)
|
||||
|
||||
return fetch(fullUrl).then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`获取文件失败,状态码:${response.status}`);
|
||||
}
|
||||
return response.blob();
|
||||
});
|
||||
}
|
||||
|
||||
export function downloadZip(files, fileName = "文件合集", callback) {
|
||||
const zip = new JSZip();
|
||||
const addedFiles = new Set();
|
||||
|
||||
const tasks = files.map((item) => {
|
||||
return getFileBlob(item.mx_mapfilename)
|
||||
.then((res) => {
|
||||
const uniqueFileName = getUniqueFileName(item.mx_filename, addedFiles);
|
||||
zip.file(uniqueFileName, res, { binary: true });
|
||||
addedFiles.add(uniqueFileName);
|
||||
})
|
||||
.catch((err) => {
|
||||
// 捕获单个文件下载失败的错误,不影响其他文件下载
|
||||
console.error(`Failed to download file: ${item.mx_filename}`, err);
|
||||
});
|
||||
});
|
||||
|
||||
// 关键:使用 Promise.all 确保所有任务完成
|
||||
return Promise.all(tasks)
|
||||
.then(() => zip.generateAsync({ type: "blob" }))
|
||||
.then((res) => {
|
||||
saveAs(res, `${fileName}.zip`);
|
||||
callback && callback();
|
||||
})
|
||||
.catch((err) => {
|
||||
callback && callback();
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
// ============================================ 本地文件上传和下载 ==========================================
|
||||
|
||||
export function joinUrl(...parts) {
|
||||
if (!parts || parts.length === 0) return "";
|
||||
|
||||
const sb = [];
|
||||
|
||||
for (let part of parts) {
|
||||
if (!part) continue;
|
||||
|
||||
part = String(part).trim();
|
||||
if (!part) continue;
|
||||
|
||||
// 替换 Windows 路径分隔符
|
||||
part = part.replace(/\\/g, "/");
|
||||
|
||||
if (sb.length === 0) {
|
||||
// 第一段:保留协议(http:// https://),去掉末尾 /
|
||||
sb.push(part.replace(/\/+$/, ""));
|
||||
} else {
|
||||
// 后续段:去掉前后 /
|
||||
sb.push(part.replace(/^\/+|\/+$/g, ""));
|
||||
}
|
||||
}
|
||||
|
||||
return sb.join("/");
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { useThrottle } from "@/utils/throttle";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { saveAs } from "file-saver";
|
||||
import JSZip from "jszip";
|
||||
import { getAliyunFileBlob } from "@/utils/file";
|
||||
import { getFileBlob } from "@/utils/file";
|
||||
import { formatToDate } from "@/utils/time";
|
||||
|
||||
const viewName = "v_container";
|
||||
@@ -189,28 +189,55 @@ async function downloadAll() {
|
||||
let finished = 0;
|
||||
let totalFiles = 0;
|
||||
|
||||
// 1. 收集所有下载任务
|
||||
const allTasks = [];
|
||||
|
||||
// ✅ 关键:箱号计数器
|
||||
const containerCountMap = new Map();
|
||||
|
||||
for (const item of records) {
|
||||
const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc");
|
||||
|
||||
const files = (fileRes.success && fileRes.data) || [];
|
||||
totalFiles += files.length;
|
||||
|
||||
// 按 年/月/日/集装箱号 生成层级文件夹
|
||||
const ymdFolderName = formatToDate(item.b_inputdatetime, "YYYY/MM/DD"); // 2026/03/27
|
||||
const containerFolderName = item.b_container_no;
|
||||
let folder = zip.folder(ymdFolderName);
|
||||
folder = folder ? folder.folder(containerFolderName) : zip.folder(containerFolderName);
|
||||
// =========================
|
||||
// ✅ 生成“箱号文件夹名称”
|
||||
// =========================
|
||||
const containerNo = item.b_container_no || "未知箱号";
|
||||
|
||||
const count = containerCountMap.get(containerNo) || 0;
|
||||
|
||||
let folderName = containerNo;
|
||||
|
||||
if (count === 1) {
|
||||
folderName = `${containerNo}_01`;
|
||||
} else if (count > 1) {
|
||||
folderName = `${containerNo}_${String(count - 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
containerCountMap.set(containerNo, count + 1);
|
||||
|
||||
// =========================
|
||||
// 生成日期目录
|
||||
// =========================
|
||||
const ymdFolderName = formatToDate(item.b_inputdatetime, "YYYY/MM/DD");
|
||||
|
||||
let folder = zip.folder(ymdFolderName);
|
||||
folder = folder ? folder.folder(folderName) : zip.folder(folderName);
|
||||
|
||||
// =========================
|
||||
// 收集下载任务
|
||||
// =========================
|
||||
files.forEach((file, idx) => {
|
||||
allTasks.push(async () => {
|
||||
try {
|
||||
const blob = await getAliyunFileBlob(file.mx_mapfilename);
|
||||
const fileName = buildFileName(item.b_container_no, idx + 1, file.mx_filename) || "未命名文件";
|
||||
const blob = await getFileBlob(file.mx_mapfilename);
|
||||
|
||||
const fileName = buildFileName(containerNo, idx + 1, file.mx_filename) || "未命名文件";
|
||||
|
||||
folder.file(fileName, blob);
|
||||
} catch (e) {
|
||||
console.error(item.b_container_no + "下载失败:", file.mx_mapfilename, e);
|
||||
console.error(`${containerNo} 下载失败:`, file.mx_mapfilename, e);
|
||||
} finally {
|
||||
finished++;
|
||||
loading.setText(`正在下载 ${finished}/${totalFiles} 个文件...`);
|
||||
@@ -219,18 +246,21 @@ async function downloadAll() {
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 并发执行下载任务,限制同时 3 个
|
||||
// =========================
|
||||
// 并发执行(限制3个)
|
||||
// =========================
|
||||
await runWithLimit(allTasks, 3);
|
||||
|
||||
loading.setText("正在生成压缩包...");
|
||||
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
|
||||
// 3. 保存压缩包
|
||||
saveAs(content, buildZipName("ALL"));
|
||||
|
||||
ElMessage.success("下载完成");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ElMessage.error("下载失败");
|
||||
} finally {
|
||||
loading.close();
|
||||
}
|
||||
@@ -246,6 +276,7 @@ async function downloadSlice() {
|
||||
let start = Math.max(1, Math.min(sliceModeForm.value.startPage || 1, allTotal.value));
|
||||
let end = Math.max(1, Math.min(sliceModeForm.value.endPage || 1, allTotal.value));
|
||||
if (start > end) [start, end] = [end, start];
|
||||
|
||||
sliceModeForm.value.startPage = start;
|
||||
sliceModeForm.value.endPage = end;
|
||||
|
||||
@@ -268,28 +299,51 @@ async function downloadSlice() {
|
||||
let finished = 0;
|
||||
const allTasks = [];
|
||||
|
||||
// ✅ 箱号计数
|
||||
const containerCountMap = new Map();
|
||||
|
||||
records.forEach((item) => {
|
||||
const ymdFolderName = formatToDate(item.b_inputdatetime, "YYYY/MM/DD");
|
||||
const containerFolderName = item.b_container_no;
|
||||
|
||||
const containerNo = item.b_container_no || "未知箱号";
|
||||
|
||||
// ===== 关键:生成文件夹名 =====
|
||||
const count = containerCountMap.get(containerNo) || 0;
|
||||
|
||||
let containerFolderName = containerNo;
|
||||
|
||||
if (count === 1) {
|
||||
containerFolderName = `${containerNo}_01`;
|
||||
} else if (count > 1) {
|
||||
containerFolderName = `${containerNo}_${String(count - 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
containerCountMap.set(containerNo, count + 1);
|
||||
|
||||
const folderPromise = loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc");
|
||||
|
||||
allTasks.push(async () => {
|
||||
const fileRes = await folderPromise;
|
||||
|
||||
if (!fileRes.success) {
|
||||
console.error("获取附件失败:", item.b_container_no);
|
||||
console.error("获取附件失败:", containerNo);
|
||||
return;
|
||||
}
|
||||
|
||||
const folder = zip.folder(ymdFolderName)?.folder(containerFolderName) || zip.folder(containerFolderName);
|
||||
let folder = zip.folder(ymdFolderName);
|
||||
folder = folder ? folder.folder(containerFolderName) : zip.folder(containerFolderName);
|
||||
|
||||
const files = fileRes.data || [];
|
||||
|
||||
const fileTasks = files.map((file, idx) => async () => {
|
||||
try {
|
||||
const blob = await getAliyunFileBlob(file.mx_mapfilename);
|
||||
const fileName = buildFileName(item.b_container_no, idx + 1, file.mx_filename) || "未命名文件";
|
||||
const blob = await getFileBlob(file.mx_mapfilename);
|
||||
|
||||
const fileName = buildFileName(containerNo, idx + 1, file.mx_filename) || "未命名文件";
|
||||
|
||||
folder.file(fileName, blob);
|
||||
} catch (e) {
|
||||
console.error(item.b_container_no + "下载失败:", file.mx_mapfilename, e);
|
||||
console.error(containerNo + "下载失败:", file.mx_mapfilename, e);
|
||||
} finally {
|
||||
finished++;
|
||||
loading.setText(`正在下载 ${finished} / ${records.length} 个文件...`);
|
||||
@@ -304,6 +358,7 @@ async function downloadSlice() {
|
||||
|
||||
loading.setText("正在生成压缩包...");
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
|
||||
saveAs(content, buildZipName("SLICE"));
|
||||
ElMessage.success("下载完成");
|
||||
} catch (e) {
|
||||
@@ -322,6 +377,7 @@ async function downloadChunk() {
|
||||
|
||||
try {
|
||||
const records = await fetchDatas();
|
||||
|
||||
if (!records?.length) {
|
||||
ElMessage.warning("无有效数据");
|
||||
return;
|
||||
@@ -334,33 +390,60 @@ async function downloadChunk() {
|
||||
|
||||
const allPageTasks = [];
|
||||
|
||||
// ✅ 箱号计数(关键)
|
||||
const containerCountMap = new Map();
|
||||
|
||||
for (let pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||
const startIndex = pageIndex * pageSize;
|
||||
const endIndex = Math.min((pageIndex + 1) * pageSize, records.length);
|
||||
const pageRecords = records.slice(startIndex, endIndex);
|
||||
|
||||
const pageFolderName = `${startIndex + 1}-${endIndex}`;
|
||||
const pageFolder = zip.folder(pageFolderName);
|
||||
|
||||
pageRecords.forEach((item) => {
|
||||
allPageTasks.push(async () => {
|
||||
const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc");
|
||||
|
||||
if (!fileRes.success) {
|
||||
console.error("获取附件失败:", item.b_container_no);
|
||||
return;
|
||||
}
|
||||
|
||||
const containerNo = item.b_container_no || "未知箱号";
|
||||
|
||||
// ===== 生成箱号文件夹名 =====
|
||||
const count = containerCountMap.get(containerNo) || 0;
|
||||
|
||||
let containerFolderName = containerNo;
|
||||
|
||||
if (count === 1) {
|
||||
containerFolderName = `${containerNo}_01`;
|
||||
} else if (count > 1) {
|
||||
containerFolderName = `${containerNo}_${String(count - 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
containerCountMap.set(containerNo, count + 1);
|
||||
|
||||
const ymdFolderName = formatToDate(item.b_inputdatetime, "YYYY/MM/DD");
|
||||
const containerFolderName = item.b_container_no;
|
||||
const folder = pageFolder.folder(ymdFolderName)?.folder(containerFolderName) || pageFolder.folder(containerFolderName);
|
||||
|
||||
let folder = pageFolder.folder(ymdFolderName);
|
||||
folder = folder
|
||||
? folder.folder(containerFolderName)
|
||||
: pageFolder.folder(containerFolderName);
|
||||
|
||||
const files = fileRes.data || [];
|
||||
|
||||
const fileTasks = files.map((file, idx) => async () => {
|
||||
try {
|
||||
const blob = await getAliyunFileBlob(file.mx_mapfilename);
|
||||
const fileName = buildFileName(item.b_container_no, idx + 1, file.mx_filename) || "未命名文件";
|
||||
const blob = await getFileBlob(file.mx_mapfilename);
|
||||
|
||||
const fileName =
|
||||
buildFileName(containerNo, idx + 1, file.mx_filename) || "未命名文件";
|
||||
|
||||
folder.file(fileName, blob);
|
||||
} catch (e) {
|
||||
console.error(item.b_container_no + "下载失败:", file.mx_mapfilename, e);
|
||||
console.error(containerNo + "下载失败:", file.mx_mapfilename, e);
|
||||
} finally {
|
||||
finished++;
|
||||
loading.setText(`正在下载 ${finished} 个文件...`);
|
||||
@@ -376,6 +459,7 @@ async function downloadChunk() {
|
||||
|
||||
loading.setText("正在生成压缩包...");
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
|
||||
saveAs(content, buildZipName("CHUNK"));
|
||||
ElMessage.success("下载完成");
|
||||
} catch (e) {
|
||||
@@ -385,7 +469,6 @@ async function downloadChunk() {
|
||||
loading.close();
|
||||
}
|
||||
}
|
||||
|
||||
function buildFileName(containerNo, index, fileName) {
|
||||
const formattedIndex = String(index).padStart(2, "0");
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<script lang="js" setup>
|
||||
import G3ContainerFile from "@/components/g3-container-file/index.vue";
|
||||
import HighDownloadDialog from "./high-download-dialog.vue";
|
||||
import UpdateDialog from "./update-dialog.vue";
|
||||
|
||||
import {
|
||||
deleteFileApi,
|
||||
loadDataApi,
|
||||
@@ -6,16 +10,16 @@ import {
|
||||
loadViewColumnsApi,
|
||||
saveObjectApi,
|
||||
} from "@/request/api";
|
||||
import { getAlignByFormat, formatValueByFormat } from "@/utils/columns";
|
||||
import { formatValueByFormat } from "@/utils/columns";
|
||||
import { updateViewTableColumns } from "@/utils/columns";
|
||||
import { downloadZip, getAliyunFileBlob } from "@/utils/file";
|
||||
import { downloadZip, getFileBlob } from "@/utils/file";
|
||||
import { formatToDate } from "@/utils/time";
|
||||
import { ElLoading, ElMessage } from "element-plus";
|
||||
import { saveAs } from "file-saver";
|
||||
import JSZip from "jszip";
|
||||
import { usePermissionStore } from "@/store/permission";
|
||||
import { ExportVxeTableExcel, exportExcel } from "@/utils/excel";
|
||||
import HighDownloadDialog from "./high-download-dialog.vue";
|
||||
import { ExportVxeTableExcel } from "@/utils/excel";
|
||||
import { buildPublicUrl } from "@/utils/file";
|
||||
|
||||
const viewName = "v_container";
|
||||
|
||||
@@ -32,6 +36,7 @@ const loading = reactive({
|
||||
});
|
||||
|
||||
const showHighDownload = ref(false);
|
||||
const showUpdate = ref(false);
|
||||
|
||||
const searchForm = ref({
|
||||
startTime: "",
|
||||
@@ -130,6 +135,14 @@ async function loadTableDatas() {
|
||||
orderField: "b_inputdatetime DESC",
|
||||
}).then((res) => {
|
||||
if (res.success) {
|
||||
console.log(res.data.rows);
|
||||
|
||||
res.data.rows.forEach((row) => {
|
||||
if (row.b_container_img) {
|
||||
row.b_container_img = buildPublicUrl(row.b_container_img);
|
||||
}
|
||||
});
|
||||
|
||||
tableData.value = res.data.rows;
|
||||
pagination.total = res.data.total;
|
||||
|
||||
@@ -204,9 +217,7 @@ async function deleteRows(rows = []) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of fileRes.data) {
|
||||
await deleteFileApi(file.subid);
|
||||
}
|
||||
await Promise.all(fileRes.data.map((file) => deleteFileApi(file.subid)));
|
||||
|
||||
const requestData = [
|
||||
{
|
||||
@@ -267,6 +278,12 @@ function handleFileShow(data) {
|
||||
showFile.value = true;
|
||||
}
|
||||
|
||||
function handleUpdateShow(data) {
|
||||
currentRow.value = data;
|
||||
|
||||
showUpdate.value = true;
|
||||
}
|
||||
|
||||
async function handleDownloadOne(data) {
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
@@ -342,29 +359,55 @@ async function handleDownloadBatch() {
|
||||
let finished = 0;
|
||||
let totalFiles = 0;
|
||||
|
||||
// 1. 收集所有下载任务
|
||||
const allTasks = [];
|
||||
|
||||
// ✅ 关键:箱号计数器
|
||||
const containerCountMap = new Map();
|
||||
|
||||
for (const item of records) {
|
||||
const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc");
|
||||
|
||||
const files = (fileRes.success && fileRes.data) || [];
|
||||
totalFiles += files.length;
|
||||
|
||||
// 按 年/月/日/集装箱号 生成层级文件夹
|
||||
const ymdFolderName = formatToDate(item.b_inputdatetime, "YYYY/MM/DD"); // 2026/03/27
|
||||
const containerFolderName = item.b_container_no;
|
||||
// =========================
|
||||
// ✅ 箱号文件夹去重逻辑
|
||||
// =========================
|
||||
const containerNo = item.b_container_no || "未知箱号";
|
||||
|
||||
const count = containerCountMap.get(containerNo) || 0;
|
||||
|
||||
let containerFolderName = containerNo;
|
||||
|
||||
if (count === 1) {
|
||||
containerFolderName = `${containerNo}_01`;
|
||||
} else if (count > 1) {
|
||||
containerFolderName = `${containerNo}_${String(count - 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
containerCountMap.set(containerNo, count + 1);
|
||||
|
||||
// =========================
|
||||
// 日期文件夹
|
||||
// =========================
|
||||
const ymdFolderName = formatToDate(item.b_inputdatetime, "YYYY/MM/DD");
|
||||
|
||||
let folder = zip.folder(ymdFolderName);
|
||||
folder = folder ? folder.folder(containerFolderName) : zip.folder(containerFolderName);
|
||||
|
||||
// =========================
|
||||
// 收集任务
|
||||
// =========================
|
||||
files.forEach((file, idx) => {
|
||||
allTasks.push(async () => {
|
||||
try {
|
||||
const blob = await getAliyunFileBlob(file.mx_mapfilename);
|
||||
const fileName =
|
||||
buildFileName(item.b_container_no, idx + 1, file.mx_filename) || "未命名文件";
|
||||
const blob = await getFileBlob(file.mx_mapfilename);
|
||||
|
||||
const fileName = buildFileName(containerNo, idx + 1, file.mx_filename) || "未命名文件";
|
||||
|
||||
folder.file(fileName, blob);
|
||||
} catch (e) {
|
||||
console.error(item.b_container_no + "下载失败:", file.mx_mapfilename, e);
|
||||
console.error(containerNo + "下载失败:", file.mx_mapfilename, e);
|
||||
} finally {
|
||||
finished++;
|
||||
loading.setText(`正在下载 ${finished}/${totalFiles} 个文件...`);
|
||||
@@ -373,23 +416,25 @@ async function handleDownloadBatch() {
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 并发执行下载任务,限制同时 3 个
|
||||
// =========================
|
||||
// 并发执行(限制3个)
|
||||
// =========================
|
||||
await runWithLimit(allTasks, 3);
|
||||
|
||||
loading.setText("正在生成压缩包...");
|
||||
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
|
||||
// 3. 保存压缩包
|
||||
saveAs(content, "批量下载");
|
||||
|
||||
ElMessage.success("下载完成");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ElMessage.error("下载失败");
|
||||
} finally {
|
||||
loading.close();
|
||||
}
|
||||
}
|
||||
|
||||
function buildFileName(containerNo, index, fileName) {
|
||||
const formattedIndex = String(index).padStart(2, "0");
|
||||
|
||||
@@ -575,29 +620,59 @@ function buildFileName(containerNo, index, fileName) {
|
||||
</vxe-column>
|
||||
<vxe-column width="220" align="center" fixed="right" title="操作">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleDownloadOne(row)"
|
||||
:disabled="!permissionStore.hasActionPermission(1)"
|
||||
>
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:cloud-download" />
|
||||
</el-icon>
|
||||
下载
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleFileShow(row)"
|
||||
:disabled="!permissionStore.hasActionPermission(3)"
|
||||
>
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:file-image" />
|
||||
</el-icon>
|
||||
图片
|
||||
</el-button>
|
||||
<el-button
|
||||
<div style="display: flex; align-items: center; justify-content: center">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleDownloadOne(row)"
|
||||
:disabled="!permissionStore.hasActionPermission(1)"
|
||||
>
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:cloud-download" />
|
||||
</el-icon>
|
||||
下载
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleFileShow(row)"
|
||||
:disabled="!permissionStore.hasActionPermission(3)"
|
||||
>
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:file-image" />
|
||||
</el-icon>
|
||||
图片
|
||||
</el-button>
|
||||
<el-dropdown>
|
||||
<el-button link type="primary" style="margin-left: 8px">
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:circle-ellipsis" />
|
||||
</el-icon>
|
||||
更多
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="handleUpdateShow(row)">
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:refresh-ccw" />
|
||||
</el-icon>
|
||||
更新
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
@click="handleDeleteRow(row)"
|
||||
:disabled="!permissionStore.hasActionPermission(2)"
|
||||
>
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:trash-2" />
|
||||
</el-icon>
|
||||
删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
<!-- <el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDeleteRow(row)"
|
||||
@@ -607,7 +682,7 @@ function buildFileName(containerNo, index, fileName) {
|
||||
<G3Icon icon="lucide:trash-2" />
|
||||
</el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</el-button> -->
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
@@ -634,14 +709,16 @@ function buildFileName(containerNo, index, fileName) {
|
||||
@close="handlePreviewClose"
|
||||
/>
|
||||
|
||||
<G3File
|
||||
<G3ContainerFile
|
||||
v-model="showFile"
|
||||
:father="currentRow.b_id"
|
||||
module-id="b_container"
|
||||
:categories="fileCategories"
|
||||
:container-no="currentRow.b_container_no"
|
||||
/>
|
||||
|
||||
<HighDownloadDialog v-model="showHighDownload" />
|
||||
<UpdateDialog v-model="showUpdate" :data="currentRow" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<script lang="js" setup>
|
||||
const visible = defineModel(false);
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const loading = reactive({
|
||||
submit: false,
|
||||
global: true,
|
||||
});
|
||||
|
||||
const form = ref({
|
||||
b_id: "",
|
||||
b_container_no: "",
|
||||
});
|
||||
|
||||
const title = computed(() => {
|
||||
if (visible.value) {
|
||||
if (props.data.b_container_no) {
|
||||
return `更新 ${props.data.b_container_no}`;
|
||||
} else {
|
||||
return "更新";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function onOpened() {
|
||||
loading.global = true;
|
||||
console.log("对话框已打开");
|
||||
}
|
||||
|
||||
function onClosed() {
|
||||
console.log("对话框已关闭");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
v-bind="$attrs"
|
||||
:title="title"
|
||||
width="500"
|
||||
@opened="onOpened"
|
||||
@closed="onClosed"
|
||||
v-model="visible"
|
||||
>
|
||||
<div class="form-container" v-loading="loading.global">
|
||||
<!-- 箱号 -->
|
||||
<div class="form-item">
|
||||
<div class="label">箱号</div>
|
||||
<div class="content">
|
||||
<el-input v-model="form.b_container_no" placeholder="请输入箱号" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button :loading="loading.submit" @click="visible = false">
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:refresh-ccw" />
|
||||
</el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button type="primary">
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:cloud-download" />
|
||||
</el-icon>
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
::deep(.el-input) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user