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;
|
private String staticPath;
|
||||||
|
|
||||||
public String getCommonPath() {
|
public String getCommonPath() {
|
||||||
return PathUtils.joinFilePath(commonDir, commonDir);
|
return PathUtils.joinFilePath(baseDir, commonDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getContainerPath() {
|
public String getContainerPath() {
|
||||||
return PathUtils.joinFilePath(commonDir, containerDir);
|
return PathUtils.joinFilePath(baseDir, containerDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTempPath() {
|
public String getTempPath() {
|
||||||
return PathUtils.joinFilePath(commonDir, tempDir);
|
return PathUtils.joinFilePath(baseDir, tempDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,6 +50,6 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
@Override
|
@Override
|
||||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
registry.addResourceHandler(fileConfig.getStaticPath() + "/**")
|
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 javax.annotation.Resource;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/file")
|
@RequestMapping("/file")
|
||||||
@@ -25,8 +22,6 @@ public class FileController {
|
|||||||
@Resource
|
@Resource
|
||||||
private DbUtils dbUtils;
|
private DbUtils dbUtils;
|
||||||
@Resource
|
@Resource
|
||||||
private FileUploadUtils fileUploadUtils;
|
|
||||||
@Resource
|
|
||||||
private FileConfig fileConfig;
|
private FileConfig fileConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,9 +29,9 @@ public class FileController {
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/uploadTemp")
|
@PostMapping("/uploadTemp")
|
||||||
public Result<Map<String, String>> uploadTemp(
|
public Result<Map<String, String>> uploadTemp(
|
||||||
@RequestParam("file") MultipartFile file,
|
@RequestParam("file") MultipartFile file,
|
||||||
@RequestParam(value = "fileName", required = false) String fileName)
|
@RequestParam(value = "fileName", required = false) String fileName)
|
||||||
throws Exception{
|
throws Exception {
|
||||||
// 1. 校验
|
// 1. 校验
|
||||||
FileUtils.checkFile(file);
|
FileUtils.checkFile(file);
|
||||||
|
|
||||||
@@ -46,6 +41,7 @@ public class FileController {
|
|||||||
// 3. 物理路径
|
// 3. 物理路径
|
||||||
String basePath = fileConfig.getTempPath(); // D:/data/file/temp/
|
String basePath = fileConfig.getTempPath(); // D:/data/file/temp/
|
||||||
|
|
||||||
|
System.out.println(basePath);
|
||||||
// 4. 保存
|
// 4. 保存
|
||||||
FileUtils.saveFile(file, basePath, uuidName);
|
FileUtils.saveFile(file, basePath, uuidName);
|
||||||
|
|
||||||
@@ -69,10 +65,7 @@ public class FileController {
|
|||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public Result<Map<String, Object>> uploadCommon(
|
public Result<Map<String, Object>> uploadCommon(
|
||||||
@RequestParam("file") MultipartFile file,
|
@RequestParam("file") MultipartFile file,
|
||||||
@RequestParam("moduleId") String moduleId,
|
@RequestParam("fileName") String fileName
|
||||||
@RequestParam("father") Long father,
|
|
||||||
@RequestParam("fileName") String fileName,
|
|
||||||
@RequestParam(value = "mxXh", required = false) Integer mxXh
|
|
||||||
) throws Exception {
|
) throws Exception {
|
||||||
// 1. 校验
|
// 1. 校验
|
||||||
FileUtils.checkFile(file);
|
FileUtils.checkFile(file);
|
||||||
@@ -97,22 +90,6 @@ public class FileController {
|
|||||||
uuidName
|
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
|
// 7. URL
|
||||||
String url = PathUtils.joinUrl(
|
String url = PathUtils.joinUrl(
|
||||||
fileConfig.getServerUrl(),
|
fileConfig.getServerUrl(),
|
||||||
@@ -129,127 +106,192 @@ public class FileController {
|
|||||||
return Result.ok(result);
|
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")
|
@PostMapping("/uploadContainer")
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public Result<Map<String, Object>> uploadContainer(
|
public Result<Map<String, Object>> uploadContainer(
|
||||||
@RequestParam("file") MultipartFile file,
|
@RequestParam("file") MultipartFile file,
|
||||||
@RequestParam("containerNo") String containerNo
|
@RequestParam("fileName") String fileName
|
||||||
) throws Exception {
|
) throws Exception {
|
||||||
|
|
||||||
// 1. 校验
|
// 1. 校验
|
||||||
FileUtils.checkFile(file);
|
FileUtils.checkFile(file);
|
||||||
|
|
||||||
// 2. 日期路径
|
// 2. 日期路径
|
||||||
String datePath = TimeUtils.todayPath(); // 2026/03/29/
|
String datePath = TimeUtils.todayPath(); // 2026/03/30/
|
||||||
|
|
||||||
// 3. 根路径
|
// 3. 根路径
|
||||||
String basePath = fileConfig.getContainerPath();
|
String basePath = fileConfig.getContainerPath();
|
||||||
String dayPath = PathUtils.joinFilePath(basePath, datePath);
|
|
||||||
|
|
||||||
String relativePath;
|
// 4. 雪花 ID(唯一)
|
||||||
String fileName;
|
long nextId = UniqueId.nextId();
|
||||||
String containerDirName;
|
|
||||||
|
|
||||||
// 🔥 4. 并发锁(同一天 + container)
|
// 5. 分桶(0-ff)
|
||||||
String lockKey = (datePath + containerNo).intern();
|
String bucket = String.format("%02x", nextId & 0xff);
|
||||||
|
|
||||||
synchronized (lockKey) {
|
// 6. 构建目录:日期 + 分桶
|
||||||
|
String fullDirPath = PathUtils.joinFilePath(
|
||||||
|
basePath,
|
||||||
|
datePath,
|
||||||
|
bucket
|
||||||
|
);
|
||||||
|
|
||||||
// 4.1 获取 container 目录(防重复)
|
// 7. 创建目录(幂等)
|
||||||
containerDirName = getAvailableContainerDir(dayPath, containerNo);
|
File dir = new File(fullDirPath);
|
||||||
|
if (!dir.exists()) {
|
||||||
String fullDirPath = PathUtils.joinFilePath(dayPath, containerDirName);
|
dir.mkdirs();
|
||||||
|
|
||||||
// 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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(
|
String url = PathUtils.joinUrl(
|
||||||
fileConfig.getServerUrl(),
|
fileConfig.getServerUrl(),
|
||||||
fileConfig.getStaticPath(),
|
fileConfig.getStaticPath(),
|
||||||
relativePath
|
relativePath
|
||||||
);
|
);
|
||||||
|
|
||||||
// 6. 返回
|
// 12. 返回
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("url", url);
|
result.put("url", url);
|
||||||
result.put("path", relativePath);
|
result.put("path", relativePath);
|
||||||
result.put("fileName", fileName);
|
result.put("uid", nextId);
|
||||||
result.put("containerDir", containerDirName);
|
result.put("fileName", newFileName);
|
||||||
|
result.put("bucket", bucket);
|
||||||
|
|
||||||
return Result.ok(result);
|
return Result.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getNextIndex(String dirPath) {
|
@PostMapping("/delete")
|
||||||
File dir = new File(dirPath);
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Result<Void> deleteFile(@RequestBody Map<String, Object> params) {
|
||||||
|
try {
|
||||||
|
// 1. 参数校验
|
||||||
|
ValidationUtils.checkRequiredFields(params, "subid");
|
||||||
|
|
||||||
if (!dir.exists()) {
|
Long subid = Long.valueOf(params.get("subid").toString());
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
File[] files = dir.listFiles();
|
// 2. 查询文件记录
|
||||||
if (files == null || files.length == 0) {
|
List<Map<String, Object>> maps = dbUtils.loadData(
|
||||||
return 1;
|
"SELECT * FROM bf_file WHERE subid = ?", subid
|
||||||
}
|
);
|
||||||
|
|
||||||
int max = 0;
|
if (maps.isEmpty()) {
|
||||||
|
return Result.ok();
|
||||||
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) {}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return max + 1;
|
Map<String, Object> objectMap = maps.get(0);
|
||||||
}
|
|
||||||
|
|
||||||
public static String getAvailableContainerDir(String basePath, String containerNo) {
|
// 3. 获取文件路径
|
||||||
File baseDir = new File(basePath);
|
String mxMapfilename = objectMap.get("mx_mapfilename") == null
|
||||||
|
? ""
|
||||||
|
: objectMap.get("mx_mapfilename").toString();
|
||||||
|
|
||||||
if (!baseDir.exists()) {
|
if (StringUtils.isBlank(mxMapfilename)) {
|
||||||
baseDir.mkdirs();
|
return Result.fail("文件路径为空");
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
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);
|
return Result.ok(result);
|
||||||
} catch (Exception e){
|
} catch (Exception e){
|
||||||
|
e.printStackTrace();
|
||||||
return Result.fail("OCR 识别失败,请联系管理员");
|
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");
|
ValidationUtils.checkRequiredFields(params, "father", "mx_xh", "mx_filename", "mx_filesize", "mx_mapfilename", "mx_moduleid");
|
||||||
|
|
||||||
String userId = UserContext.get().getUserId();
|
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_inputuser_id",userId);
|
||||||
params.put("mx_inputdatetime", TimeUtils.getDateTime());
|
params.put("mx_inputdatetime", TimeUtils.getDateTime());
|
||||||
|
|
||||||
dbUtils.insert("bf_file","subid",params);
|
dbUtils.insert("bf_file","subid",params);
|
||||||
|
|
||||||
return Result.ok(nextId);
|
return Result.ok(subid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/delete")
|
// @PostMapping("/delete")
|
||||||
@Transactional(rollbackFor = Exception.class)
|
// @Transactional(rollbackFor = Exception.class)
|
||||||
public Result<Void> deleteFile(@RequestBody Map<String, Object> params) {
|
// public Result<Void> deleteFile(@RequestBody Map<String, Object> params) {
|
||||||
ValidationUtils.checkRequiredFields(params, "subid", "tableName", "bucketName");
|
// ValidationUtils.checkRequiredFields(params, "subid", "tableName", "bucketName");
|
||||||
|
//
|
||||||
String tableName = params.get("tableName").toString();
|
// String tableName = params.get("tableName").toString();
|
||||||
String bucketName = params.get("bucketName").toString();
|
// String bucketName = params.get("bucketName").toString();
|
||||||
String id = params.get("subid").toString();
|
// String id = params.get("subid").toString();
|
||||||
|
//
|
||||||
// 查询数据
|
// // 查询数据
|
||||||
List<Map<String, Object>> maps = dbUtils.loadData("SELECT * FROM " + tableName + " WHERE subid = ?", id);
|
// List<Map<String, Object>> maps = dbUtils.loadData("SELECT * FROM " + tableName + " WHERE subid = ?", id);
|
||||||
|
//
|
||||||
// 如果没有数据,直接返回删除成功
|
// // 如果没有数据,直接返回删除成功
|
||||||
if (ObjectUtils.isEmpty(maps)) {
|
// if (ObjectUtils.isEmpty(maps)) {
|
||||||
return Result.ok("删除成功");
|
// return Result.ok("删除成功");
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
String mxMapfilename = maps.get(0).get("mx_mapfilename").toString();
|
// String mxMapfilename = maps.get(0).get("mx_mapfilename").toString();
|
||||||
String orgId = UserContext.get().getOrgId();
|
// String orgId = UserContext.get().getOrgId();
|
||||||
int index = mxMapfilename.indexOf(orgId + "/");
|
// int index = mxMapfilename.indexOf(orgId + "/");
|
||||||
|
//
|
||||||
// 如果没找到,就转换orgId为大写
|
// // 如果没找到,就转换orgId为大写
|
||||||
if (index < 0) {
|
// if (index < 0) {
|
||||||
orgId = orgId.toUpperCase();
|
// orgId = orgId.toUpperCase();
|
||||||
index = mxMapfilename.indexOf(orgId + "/");
|
// index = mxMapfilename.indexOf(orgId + "/");
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
String objectName = mxMapfilename.substring(index);
|
// String objectName = mxMapfilename.substring(index);
|
||||||
|
//
|
||||||
// 删除记录
|
// // 删除记录
|
||||||
Map<String, Object> deleteParams = new HashMap<>();
|
// Map<String, Object> deleteParams = new HashMap<>();
|
||||||
deleteParams.put("subid", id);
|
// deleteParams.put("subid", id);
|
||||||
int deleteCount = dbUtils.delete(tableName, deleteParams);
|
// int deleteCount = dbUtils.delete(tableName, deleteParams);
|
||||||
|
//
|
||||||
// 如果删除成功,发送删除请求
|
// // 如果删除成功,发送删除请求
|
||||||
if (deleteCount > 0) {
|
// if (deleteCount > 0) {
|
||||||
if(StringUtils.isBlank(objectName)){
|
// if(StringUtils.isBlank(objectName)){
|
||||||
return Result.fail("删除失败,文件格式有误");
|
// return Result.fail("删除失败,文件格式有误");
|
||||||
}
|
// }
|
||||||
String requestUrl = String.format("http://g3oss.g3soft.cn:8082/delete?bucketName=%s&objectName=%s", bucketName, objectName);
|
// String requestUrl = String.format("http://g3oss.g3soft.cn:8082/delete?bucketName=%s&objectName=%s", bucketName, objectName);
|
||||||
try {
|
// try {
|
||||||
HttpUtils.get(requestUrl);
|
// HttpUtils.get(requestUrl);
|
||||||
} catch (Exception e) {
|
// } catch (Exception e) {
|
||||||
throw new RuntimeException("删除文件失败",e);
|
// throw new RuntimeException("删除文件失败",e);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
return Result.ok();
|
// return Result.ok();
|
||||||
}
|
// }
|
||||||
|
|
||||||
@PostMapping("/deleteAliyunFile")
|
@PostMapping("/deleteAliyunFile")
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@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("上传文件不能为空");
|
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();
|
part = part.trim();
|
||||||
|
|
||||||
|
// 替换 Windows 路径分隔符为 URL 的 '/'
|
||||||
|
part = part.replace("\\", "/");
|
||||||
|
|
||||||
if (sb.length() == 0) {
|
if (sb.length() == 0) {
|
||||||
// 第一段:保留协议(http://)
|
// 第一段:保留协议(http://)
|
||||||
sb.append(part.replaceAll("/+$", ""));
|
sb.append(part.replaceAll("/+$", ""));
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
VITE_APP_PORT = 8080
|
VITE_APP_PORT = 8082
|
||||||
|
|
||||||
VITE_ALIYUN_BUCKET = 'g3hd'
|
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>
|
<script lang="ts" setup>
|
||||||
import type { UploadFile, UploadFileItem } from 'sard-uniapp'
|
import type { UploadFile, UploadFileItem } from 'sard-uniapp'
|
||||||
import { dialog, isImageUrl, isVideoUrl, toast } 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 { downloadFile, getFileExtension, imageCompress, isImage } from '@/utils/file'
|
||||||
import { uploadToOss } from '@/utils/oss'
|
|
||||||
|
|
||||||
const boxImage = ref(null)
|
const boxImage = ref(null)
|
||||||
|
|
||||||
@@ -14,8 +13,8 @@ const showResult = ref(false)
|
|||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
b_id: 0,
|
b_id: 0,
|
||||||
containerNo: '',
|
b_container_no: '',
|
||||||
containerImageUrl: '',
|
b_container_img: '',
|
||||||
ocrScore: 0.00,
|
ocrScore: 0.00,
|
||||||
ocrNo: '',
|
ocrNo: '',
|
||||||
})
|
})
|
||||||
@@ -53,11 +52,14 @@ function handleChooseBox() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
imageCompress(tempFile.path).then((compressRes) => {
|
imageCompress(tempFile.path).then((compressRes) => {
|
||||||
console.log(compressRes)
|
form.value.b_container_img = compressRes.path
|
||||||
uploadTempFileApi(compressRes.path, tempFile.name).then((res) => {
|
|
||||||
|
uploadTempFileApi(compressRes.path, {
|
||||||
|
fileName: tempFile.name,
|
||||||
|
}).then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
recognizeOcrBoxApi({
|
recognizeOcrBoxApi({
|
||||||
fileUrl: res.data.tempUrl,
|
fileUrl: res.data.url,
|
||||||
}).then((ocrRes) => {
|
}).then((ocrRes) => {
|
||||||
if (ocrRes.success) {
|
if (ocrRes.success) {
|
||||||
toast.success('识别成功,请检查箱号是否正确')
|
toast.success('识别成功,请检查箱号是否正确')
|
||||||
@@ -65,7 +67,7 @@ function handleChooseBox() {
|
|||||||
const score = Number(ocrRes?.data?.score ?? 0)
|
const score = Number(ocrRes?.data?.score ?? 0)
|
||||||
form.value.ocrScore = Math.floor(score * 100) / 100
|
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
|
form.value.ocrNo = ocrRes.data.boxNumber
|
||||||
|
|
||||||
isError.value = false
|
isError.value = false
|
||||||
@@ -88,29 +90,17 @@ const computedOcrScore = computed(() => {
|
|||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
boxImage.value = null
|
boxImage.value = null
|
||||||
form.value.containerImageUrl = ''
|
form.value.b_container_no = ''
|
||||||
form.value.containerNo = ''
|
|
||||||
form.value.ocrNo = ''
|
form.value.ocrNo = ''
|
||||||
form.value.ocrScore = 0.00
|
form.value.ocrScore = 0.00
|
||||||
|
|
||||||
|
showResult.value = false
|
||||||
|
isError.value = false
|
||||||
|
ocrFinish.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeleteBoxImage() {
|
function handleDeleteBoxImage() {
|
||||||
if (!form.value.containerImageUrl) {
|
resetForm()
|
||||||
resetForm()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.loading('正在删除中 . . .', {
|
|
||||||
overlay: true,
|
|
||||||
transparent: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
deleteAliyunFileApi(form.value.containerImageUrl).then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
resetForm()
|
|
||||||
toast.success('删除成功')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onItemClick(fileItem: UploadFileItem) {
|
function onItemClick(fileItem: UploadFileItem) {
|
||||||
@@ -136,9 +126,7 @@ function beforeRead(file: UploadFile): boolean | Promise<UploadFile> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function beforeRemove(index: number, fileItem: UploadFileItem): boolean | Promise<void> {
|
function beforeRemove(_index: number, _fileItem: UploadFileItem): boolean | Promise<void> {
|
||||||
console.log(fileItem)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
dialog.confirm({
|
dialog.confirm({
|
||||||
title: '提示',
|
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() {
|
async function handleConfirm() {
|
||||||
if (form.value.containerImageUrl === '') {
|
if (form.value.b_container_img === '') {
|
||||||
toast.fail('集装箱号图片不能为空')
|
toast.fail('集装箱号图片不能为空')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.value.containerNo === '') {
|
if (form.value.b_container_no === '') {
|
||||||
toast.fail('集装箱号不能为空')
|
toast.fail('集装箱号不能为空')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateContainerNo(form.value.containerNo)) {
|
if (!validateContainerNo(form.value.b_container_no)) {
|
||||||
toast.fail('集装箱号不符合规则,请检查')
|
toast.fail('集装箱号不符合规则,请检查')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -181,82 +159,88 @@ async function handleConfirm() {
|
|||||||
transparent: false,
|
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
|
||||||
|
|
||||||
// // 图片已经上传成功,保存主表即可
|
fileInserts.push({
|
||||||
// const mainTable = {
|
subid: res.data.uid,
|
||||||
// b_id: form.value.b_id,
|
father: form.value.b_id,
|
||||||
// b_container_no: form.value.containerNo,
|
mx_xh: 0,
|
||||||
// b_container_img: form.value.containerImageUrl,
|
mx_filename: res.data.fileName,
|
||||||
// b_ocr_no: form.value.ocrNo,
|
mx_filesize: boxImage.value.size,
|
||||||
// b_ocr_score: form.value.ocrScore || 0.00,
|
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) => {
|
if (res.success) {
|
||||||
// const uniqueRes = await getUniqueIdApi()
|
const indexNumber = index + 2
|
||||||
// const indexNumber = index + 2
|
|
||||||
|
|
||||||
// fileInserts.push({
|
fileInserts.push({
|
||||||
// subid: uniqueRes.data,
|
subid: res.data.uid,
|
||||||
// father: form.value.b_id,
|
father: form.value.b_id,
|
||||||
// mx_xh: indexNumber,
|
mx_xh: indexNumber,
|
||||||
// mx_filename: buildFileName(containerNo, indexNumber, fileItem.file.name),
|
mx_filename: res.data.fileName,
|
||||||
// mx_filesize: fileItem.file.size,
|
mx_filesize: fileItem.file.size,
|
||||||
// mx_fileext: getFileExtension(fileItem.file.name),
|
mx_fileext: getFileExtension(fileItem.file.name),
|
||||||
// mx_mapfilename: fileItem.url,
|
mx_mapfilename: res.data.path,
|
||||||
// mx_moduleid: 'b_container',
|
mx_moduleid: 'b_container',
|
||||||
// mx_cate_id: '1',
|
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({
|
saveObjectApi(reqData).then((res) => {
|
||||||
// subid: uniqueRes.data,
|
if (res.success) {
|
||||||
// father: form.value.b_id,
|
toast.success('保存成功')
|
||||||
// mx_xh: 0,
|
tipContainerNo.value = form.value.b_container_no
|
||||||
// mx_filename: buildFileName(containerNo, 1, boxImage.value.name),
|
showResult.value = true
|
||||||
// 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()}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 集装箱号校验
|
// 集装箱号校验
|
||||||
@@ -302,7 +286,7 @@ async function continueAdd() {
|
|||||||
<view style="display:flex;align-items: center;">
|
<view style="display:flex;align-items: center;">
|
||||||
<sar-icon family="iconfont" name="iconyuanbanben_OCR" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
|
<sar-icon family="iconfont" name="iconyuanbanben_OCR" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
|
||||||
<text>
|
<text>
|
||||||
集装箱号(OCR识别)
|
箱门照(OCR识别)
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -367,7 +351,7 @@ async function continueAdd() {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<sar-input
|
<sar-input
|
||||||
v-model="form.containerNo"
|
v-model="form.b_container_no"
|
||||||
placeholder="请输入箱号"
|
placeholder="请输入箱号"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
@@ -390,7 +374,6 @@ async function continueAdd() {
|
|||||||
:before-read="beforeRead"
|
:before-read="beforeRead"
|
||||||
:before-remove="beforeRemove"
|
:before-remove="beforeRemove"
|
||||||
@item-click="onItemClick"
|
@item-click="onItemClick"
|
||||||
@remove="onRemove"
|
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</wd-cell-group>
|
</wd-cell-group>
|
||||||
|
|||||||
@@ -95,6 +95,13 @@
|
|||||||
"navigationBarTitleText": "集装箱装货采集",
|
"navigationBarTitleText": "集装箱装货采集",
|
||||||
"navigationStyle": "default"
|
"navigationStyle": "default"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "upload/edit",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "集装箱装货采集(更新)",
|
||||||
|
"navigationStyle": "default"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { toast } from 'sard-uniapp'
|
import { toast } from 'sard-uniapp'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { getOrgBaseUrl } from '@/request'
|
||||||
import { loadPageDataApi } from '@/request/api'
|
import { loadPageDataApi } from '@/request/api'
|
||||||
import { useUserStore } from '@/store'
|
import { useUserStore } from '@/store'
|
||||||
import { deepEqual, groupFieldsBySpan } from '@/utils/common'
|
import { groupFieldsBySpan } from '@/utils/common'
|
||||||
import { getCurrentDate } from '@/utils/time'
|
import { buildPubicUrl } from '@/utils/file'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const pagingRef = ref<any>(null)
|
const pagingRef = ref<any>(null)
|
||||||
const dataList = ref<any[]>([])
|
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('')
|
const searchValue = ref('')
|
||||||
|
|
||||||
@@ -34,15 +29,18 @@ function handleSearch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 模拟分页请求
|
// 模拟分页请求
|
||||||
function queryList(pageNo: number, pageSize: number) {
|
async function queryList(pageNo: number, pageSize: number) {
|
||||||
toast.loading('正在加载中')
|
toast.loading('正在加载中')
|
||||||
|
|
||||||
|
const orgid = userStore.userInfo.orgid
|
||||||
const userid = userStore.userInfo.userid
|
const userid = userStore.userInfo.userid
|
||||||
|
|
||||||
|
const orgUrl = await getOrgBaseUrl(orgid)
|
||||||
|
|
||||||
let searchCondition = `2 > 1 AND b_inputuser_id = '${userid}'`
|
let searchCondition = `2 > 1 AND b_inputuser_id = '${userid}'`
|
||||||
|
|
||||||
if (searchValue.value) {
|
if (searchValue.value) {
|
||||||
searchCondition += `b_container_no LIKE '%${searchValue.value}%' `
|
searchCondition += `AND b_container_no LIKE '%${searchValue.value}%' `
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPageDataApi({
|
loadPageDataApi({
|
||||||
@@ -54,8 +52,11 @@ function queryList(pageNo: number, pageSize: number) {
|
|||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const rows = res.data.rows || []
|
const rows = res.data.rows || []
|
||||||
console.log(rows)
|
|
||||||
if (rows) {
|
if (rows) {
|
||||||
|
rows.forEach((item) => {
|
||||||
|
item.b_container_img = buildPubicUrl(orgUrl, item.b_container_img)
|
||||||
|
})
|
||||||
pagingRef.value.complete(rows)
|
pagingRef.value.complete(rows)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -64,12 +65,10 @@ function queryList(pageNo: number, pageSize: number) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleToFile(itemData) {
|
function handleToDetail(item) {
|
||||||
const searchids = [itemData.b_id, itemData.father]
|
// 跳转到详情页
|
||||||
.filter(Boolean)
|
|
||||||
.join(',')
|
|
||||||
uni.navigateTo({
|
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>
|
</script>
|
||||||
@@ -96,13 +95,13 @@ function handleToFile(itemData) {
|
|||||||
v-for="(item, index) in dataList"
|
v-for="(item, index) in dataList"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="list-item"
|
class="list-item"
|
||||||
|
@click="handleToDetail(item)"
|
||||||
>
|
>
|
||||||
<view class="list-item__header">
|
<view class="list-item__header">
|
||||||
<view class="list-item__main">
|
<view class="list-item__main">
|
||||||
<sar-image
|
<wd-img
|
||||||
:src="item.b_container_img"
|
width="160rpx" height="160rpx" :src="item.b_container_img" :enable-preview="true"
|
||||||
width="160rpx"
|
@click.stop
|
||||||
height="160rpx"
|
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -195,6 +194,12 @@ page {
|
|||||||
position: relative;
|
position: relative;
|
||||||
padding: 16rpx;
|
padding: 16rpx;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
&.list-item--desktop {
|
&.list-item--desktop {
|
||||||
.list-item__header {
|
.list-item__header {
|
||||||
max-width: 1000rpx;
|
max-width: 1000rpx;
|
||||||
|
|||||||
@@ -45,8 +45,6 @@ export function recognizeOcrBoxApi(params: OcrBoxRequest) {
|
|||||||
export function deleteFileApi(subid) {
|
export function deleteFileApi(subid) {
|
||||||
return httpPost(`file/delete`, {
|
return httpPost(`file/delete`, {
|
||||||
subid,
|
subid,
|
||||||
tableName: 'bf_file',
|
|
||||||
bucketName: aliyunBucket,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,8 +74,8 @@ export function getUniqueIdApi() {
|
|||||||
interface LoadDataParams {
|
interface LoadDataParams {
|
||||||
viewName: string
|
viewName: string
|
||||||
searchCondition: string
|
searchCondition: string
|
||||||
orderField: string
|
orderField?: string
|
||||||
searchColumns: string[]
|
searchColumns?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadDataApi(params: LoadDataParams) {
|
export function loadDataApi(params: LoadDataParams) {
|
||||||
@@ -109,6 +107,28 @@ export function loadPageDataApi(params: PageParams) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uploadTempFileApi(filePath: string, fileName: string) {
|
interface UpdateTempFileParams {
|
||||||
return httpUpload('/file/uploadTemp', filePath, fileName)
|
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 * as dbJson from '@/static/db.json'
|
||||||
import { useUserStore } from '@/store'
|
import { useUserStore } from '@/store'
|
||||||
import { joinUrl } from '@/utils/common'
|
import { joinUrl } from '@/utils/common'
|
||||||
|
|
||||||
import { isH5 } from '@/utils/platform'
|
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
|
* 获取 baseUrl + headers
|
||||||
@@ -200,7 +243,6 @@ function request<T = any>(
|
|||||||
export function httpUpload<T = any>(
|
export function httpUpload<T = any>(
|
||||||
url: string,
|
url: string,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
fileName: string,
|
|
||||||
formData?: Record<string, any>,
|
formData?: Record<string, any>,
|
||||||
option?: UniApp.UploadFileOption,
|
option?: UniApp.UploadFileOption,
|
||||||
) {
|
) {
|
||||||
@@ -217,17 +259,12 @@ export function httpUpload<T = any>(
|
|||||||
|
|
||||||
const fullUrl = joinUrl(baseUrl, url)
|
const fullUrl = joinUrl(baseUrl, url)
|
||||||
|
|
||||||
const mergedFormData = {
|
|
||||||
...formData,
|
|
||||||
fileName,
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise<IResData<T>>((resolve, reject) => {
|
return new Promise<IResData<T>>((resolve, reject) => {
|
||||||
task = uni.uploadFile({
|
task = uni.uploadFile({
|
||||||
url: fullUrl,
|
url: fullUrl,
|
||||||
filePath,
|
filePath,
|
||||||
name: 'file',
|
name: 'file',
|
||||||
formData: mergedFormData,
|
formData,
|
||||||
header: {
|
header: {
|
||||||
...headers,
|
...headers,
|
||||||
...(option?.header || {}),
|
...(option?.header || {}),
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { toast } from 'sard-uniapp'
|
import { toast } from 'sard-uniapp'
|
||||||
|
import { getOrgBaseUrl } from '@/request'
|
||||||
|
import { useUserStore } from '@/store'
|
||||||
import { getOssKey } from './oss'
|
import { getOssKey } from './oss'
|
||||||
import { isApp, isH5, isMpWeixin } from './platform'
|
import { isApp, isH5, isMpWeixin } from './platform'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
const aliyunBucket = import.meta.env.VITE_ALIYUN_BUCKET
|
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`
|
else return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传文件到阿里云 OSS(优化版)
|
* 上传文件到阿里云 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
|
||||||
# 后端 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
|
PUBLIC_ALIYUN_BUCKET = g3hd
|
||||||
+1
@@ -39,6 +39,7 @@ declare module 'vue' {
|
|||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
ElTree: typeof import('element-plus/es')['ElTree']
|
ElTree: typeof import('element-plus/es')['ElTree']
|
||||||
G3Column: typeof import('./src/components/g3-table/g3-column.vue')['default']
|
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']
|
G3Detail: typeof import('./src/components/g3-detail/index.vue')['default']
|
||||||
G3Error: typeof import('./src/components/g3-error/index.vue')['default']
|
G3Error: typeof import('./src/components/g3-error/index.vue')['default']
|
||||||
G3File: typeof import('./src/components/g3-file/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,
|
getAliyunFileBlob,
|
||||||
getFileExtension,
|
getFileExtension,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
|
buildPublicUrl,
|
||||||
} from "@/utils/file";
|
} from "@/utils/file";
|
||||||
import { loadDataApi, saveObjectKeysApi, deleteFileApi } from "@/request/api";
|
import {
|
||||||
|
loadDataApi,
|
||||||
|
insertFileApi,
|
||||||
|
saveObjectApi,
|
||||||
|
deleteFileApi,
|
||||||
|
uploadCommonFileApi,
|
||||||
|
} from "@/request/api";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
father: {
|
father: {
|
||||||
@@ -158,7 +165,7 @@ async function handleConfirmUpdate() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await saveObjectKeysApi(requestData);
|
const response = await saveObjectApi(requestData);
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
ElMessage.success("文件名更新成功");
|
ElMessage.success("文件名更新成功");
|
||||||
@@ -239,8 +246,15 @@ async function loadFiles() {
|
|||||||
const res = await loadDataApi("bf_file", searchCondition, "mx_xh");
|
const res = await loadDataApi("bf_file", searchCondition, "mx_xh");
|
||||||
|
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
fileList.value = res.data;
|
let datas = res.data.map((item) => {
|
||||||
oldFileList.value = JSON.parse(JSON.stringify(res.data));
|
return {
|
||||||
|
...item,
|
||||||
|
mx_mapfilename: buildPublicUrl(item.mx_mapfilename),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
fileList.value = datas;
|
||||||
|
oldFileList.value = JSON.parse(JSON.stringify(datas));
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -588,45 +602,69 @@ async function deleteFiles(files) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存
|
||||||
// 保存
|
// 保存
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
loadingTip.value = "正在保存中 . . .";
|
loadingTip.value = "正在保存中 . . .";
|
||||||
|
|
||||||
const { insertdata, updatedata, deletedata, hasChanges } = compareAndClassifyData(
|
try {
|
||||||
fileList.value,
|
const { insertdata, updatedata, deletedata, hasChanges } = compareAndClassifyData(
|
||||||
oldFileList.value,
|
fileList.value,
|
||||||
"subid",
|
oldFileList.value,
|
||||||
["_content", "_status", "checked"],
|
"subid",
|
||||||
);
|
["_content", "_status", "checked"],
|
||||||
|
);
|
||||||
|
|
||||||
// 处理新增的文件
|
// 没有变化直接结束
|
||||||
for (const item of insertdata) {
|
if (!hasChanges) {
|
||||||
try {
|
ElMessage.info("没有数据变更");
|
||||||
await uploadFile(
|
return;
|
||||||
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} 上传失败,请重试`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
async function handleDownloadZip() {
|
||||||
if (checkedFiles.value.length < 2) {
|
if (checkedFiles.value.length < 2) {
|
||||||
ElMessage.warning("打包文件数量不得少于2");
|
ElMessage.warning("打包文件数量不得少于2");
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { httpPost, httpUpload } from ".";
|
import { httpPost, httpUpload } from ".";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 登录
|
// 登录
|
||||||
export function loginApi(orgid, userid, password) {
|
export function loginApi(orgid, userid, password) {
|
||||||
return httpPost(`/auth/login`, {
|
return httpPost(`/auth/login`, {
|
||||||
@@ -190,11 +188,31 @@ export function insertFileApi(params) {
|
|||||||
return httpPost("/file/insert", 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) {
|
export function deleteFileApi(subid) {
|
||||||
return httpPost("/file/delete", {
|
return httpPost("/file/delete", {
|
||||||
subid,
|
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 =============================
|
// ============================= file =============================
|
||||||
@@ -120,19 +120,17 @@ export function httpPost(url, data, config) {
|
|||||||
* @param {Object} config - axios 配置(可选)
|
* @param {Object} config - axios 配置(可选)
|
||||||
* @returns {Promise<any>}
|
* @returns {Promise<any>}
|
||||||
*/
|
*/
|
||||||
export function httpUpload(url, file, extraData = {}, config = {}) {
|
export async function httpUpload(url, file, extraData = {}, config = {}) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
// 兼容单文件 & 多文件
|
|
||||||
if (Array.isArray(file)) {
|
if (Array.isArray(file)) {
|
||||||
file.forEach((f) => {
|
for (const f of file) {
|
||||||
formData.append("file", f);
|
await appendFile(formData, f);
|
||||||
});
|
}
|
||||||
} else {
|
} else {
|
||||||
formData.append("file", file);
|
await appendFile(formData, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 追加额外参数
|
|
||||||
Object.keys(extraData).forEach((key) => {
|
Object.keys(extraData).forEach((key) => {
|
||||||
formData.append(key, extraData[key]);
|
formData.append(key, extraData[key]);
|
||||||
});
|
});
|
||||||
@@ -147,6 +145,24 @@ export function httpUpload(url, file, extraData = {}, config = {}) {
|
|||||||
.then((res) => res.data);
|
.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() {
|
export function reset401Flag() {
|
||||||
hasShown401 = false;
|
hasShown401 = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,7 +203,8 @@ export function getFileExtension(fileName) {
|
|||||||
return fileExt;
|
return fileExt;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadZip(files, fileName = "文件合集", callback) {
|
|
||||||
|
export function downloadOssZip(files, fileName = "文件合集", callback) {
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
const addedFiles = new Set();
|
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 { ElMessage } from "element-plus";
|
||||||
import { saveAs } from "file-saver";
|
import { saveAs } from "file-saver";
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
import { getAliyunFileBlob } from "@/utils/file";
|
import { getFileBlob } from "@/utils/file";
|
||||||
import { formatToDate } from "@/utils/time";
|
import { formatToDate } from "@/utils/time";
|
||||||
|
|
||||||
const viewName = "v_container";
|
const viewName = "v_container";
|
||||||
@@ -189,28 +189,55 @@ async function downloadAll() {
|
|||||||
let finished = 0;
|
let finished = 0;
|
||||||
let totalFiles = 0;
|
let totalFiles = 0;
|
||||||
|
|
||||||
// 1. 收集所有下载任务
|
|
||||||
const allTasks = [];
|
const allTasks = [];
|
||||||
|
|
||||||
|
// ✅ 关键:箱号计数器
|
||||||
|
const containerCountMap = new Map();
|
||||||
|
|
||||||
for (const item of records) {
|
for (const item of records) {
|
||||||
const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc");
|
const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc");
|
||||||
|
|
||||||
const files = (fileRes.success && fileRes.data) || [];
|
const files = (fileRes.success && fileRes.data) || [];
|
||||||
totalFiles += files.length;
|
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);
|
const containerNo = item.b_container_no || "未知箱号";
|
||||||
folder = folder ? folder.folder(containerFolderName) : zip.folder(containerFolderName);
|
|
||||||
|
|
||||||
|
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) => {
|
files.forEach((file, idx) => {
|
||||||
allTasks.push(async () => {
|
allTasks.push(async () => {
|
||||||
try {
|
try {
|
||||||
const blob = await getAliyunFileBlob(file.mx_mapfilename);
|
const blob = await getFileBlob(file.mx_mapfilename);
|
||||||
const fileName = buildFileName(item.b_container_no, idx + 1, file.mx_filename) || "未命名文件";
|
|
||||||
|
const fileName = buildFileName(containerNo, idx + 1, file.mx_filename) || "未命名文件";
|
||||||
|
|
||||||
folder.file(fileName, blob);
|
folder.file(fileName, blob);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(item.b_container_no + "下载失败:", file.mx_mapfilename, e);
|
console.error(`${containerNo} 下载失败:`, file.mx_mapfilename, e);
|
||||||
} finally {
|
} finally {
|
||||||
finished++;
|
finished++;
|
||||||
loading.setText(`正在下载 ${finished}/${totalFiles} 个文件...`);
|
loading.setText(`正在下载 ${finished}/${totalFiles} 个文件...`);
|
||||||
@@ -219,18 +246,21 @@ async function downloadAll() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 并发执行下载任务,限制同时 3 个
|
// =========================
|
||||||
|
// 并发执行(限制3个)
|
||||||
|
// =========================
|
||||||
await runWithLimit(allTasks, 3);
|
await runWithLimit(allTasks, 3);
|
||||||
|
|
||||||
loading.setText("正在生成压缩包...");
|
loading.setText("正在生成压缩包...");
|
||||||
|
|
||||||
const content = await zip.generateAsync({ type: "blob" });
|
const content = await zip.generateAsync({ type: "blob" });
|
||||||
|
|
||||||
// 3. 保存压缩包
|
|
||||||
saveAs(content, buildZipName("ALL"));
|
saveAs(content, buildZipName("ALL"));
|
||||||
|
|
||||||
ElMessage.success("下载完成");
|
ElMessage.success("下载完成");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
ElMessage.error("下载失败");
|
||||||
} finally {
|
} finally {
|
||||||
loading.close();
|
loading.close();
|
||||||
}
|
}
|
||||||
@@ -246,6 +276,7 @@ async function downloadSlice() {
|
|||||||
let start = Math.max(1, Math.min(sliceModeForm.value.startPage || 1, allTotal.value));
|
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));
|
let end = Math.max(1, Math.min(sliceModeForm.value.endPage || 1, allTotal.value));
|
||||||
if (start > end) [start, end] = [end, start];
|
if (start > end) [start, end] = [end, start];
|
||||||
|
|
||||||
sliceModeForm.value.startPage = start;
|
sliceModeForm.value.startPage = start;
|
||||||
sliceModeForm.value.endPage = end;
|
sliceModeForm.value.endPage = end;
|
||||||
|
|
||||||
@@ -268,28 +299,51 @@ async function downloadSlice() {
|
|||||||
let finished = 0;
|
let finished = 0;
|
||||||
const allTasks = [];
|
const allTasks = [];
|
||||||
|
|
||||||
|
// ✅ 箱号计数
|
||||||
|
const containerCountMap = new Map();
|
||||||
|
|
||||||
records.forEach((item) => {
|
records.forEach((item) => {
|
||||||
const ymdFolderName = formatToDate(item.b_inputdatetime, "YYYY/MM/DD");
|
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");
|
const folderPromise = loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc");
|
||||||
|
|
||||||
allTasks.push(async () => {
|
allTasks.push(async () => {
|
||||||
const fileRes = await folderPromise;
|
const fileRes = await folderPromise;
|
||||||
|
|
||||||
if (!fileRes.success) {
|
if (!fileRes.success) {
|
||||||
console.error("获取附件失败:", item.b_container_no);
|
console.error("获取附件失败:", containerNo);
|
||||||
return;
|
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 files = fileRes.data || [];
|
||||||
|
|
||||||
const fileTasks = files.map((file, idx) => async () => {
|
const fileTasks = files.map((file, idx) => async () => {
|
||||||
try {
|
try {
|
||||||
const blob = await getAliyunFileBlob(file.mx_mapfilename);
|
const blob = await getFileBlob(file.mx_mapfilename);
|
||||||
const fileName = buildFileName(item.b_container_no, idx + 1, file.mx_filename) || "未命名文件";
|
|
||||||
|
const fileName = buildFileName(containerNo, idx + 1, file.mx_filename) || "未命名文件";
|
||||||
|
|
||||||
folder.file(fileName, blob);
|
folder.file(fileName, blob);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(item.b_container_no + "下载失败:", file.mx_mapfilename, e);
|
console.error(containerNo + "下载失败:", file.mx_mapfilename, e);
|
||||||
} finally {
|
} finally {
|
||||||
finished++;
|
finished++;
|
||||||
loading.setText(`正在下载 ${finished} / ${records.length} 个文件...`);
|
loading.setText(`正在下载 ${finished} / ${records.length} 个文件...`);
|
||||||
@@ -304,6 +358,7 @@ async function downloadSlice() {
|
|||||||
|
|
||||||
loading.setText("正在生成压缩包...");
|
loading.setText("正在生成压缩包...");
|
||||||
const content = await zip.generateAsync({ type: "blob" });
|
const content = await zip.generateAsync({ type: "blob" });
|
||||||
|
|
||||||
saveAs(content, buildZipName("SLICE"));
|
saveAs(content, buildZipName("SLICE"));
|
||||||
ElMessage.success("下载完成");
|
ElMessage.success("下载完成");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -322,6 +377,7 @@ async function downloadChunk() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const records = await fetchDatas();
|
const records = await fetchDatas();
|
||||||
|
|
||||||
if (!records?.length) {
|
if (!records?.length) {
|
||||||
ElMessage.warning("无有效数据");
|
ElMessage.warning("无有效数据");
|
||||||
return;
|
return;
|
||||||
@@ -334,33 +390,60 @@ async function downloadChunk() {
|
|||||||
|
|
||||||
const allPageTasks = [];
|
const allPageTasks = [];
|
||||||
|
|
||||||
|
// ✅ 箱号计数(关键)
|
||||||
|
const containerCountMap = new Map();
|
||||||
|
|
||||||
for (let pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
for (let pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||||
const startIndex = pageIndex * pageSize;
|
const startIndex = pageIndex * pageSize;
|
||||||
const endIndex = Math.min((pageIndex + 1) * pageSize, records.length);
|
const endIndex = Math.min((pageIndex + 1) * pageSize, records.length);
|
||||||
const pageRecords = records.slice(startIndex, endIndex);
|
const pageRecords = records.slice(startIndex, endIndex);
|
||||||
|
|
||||||
const pageFolderName = `${startIndex + 1}-${endIndex}`;
|
const pageFolderName = `${startIndex + 1}-${endIndex}`;
|
||||||
const pageFolder = zip.folder(pageFolderName);
|
const pageFolder = zip.folder(pageFolderName);
|
||||||
|
|
||||||
pageRecords.forEach((item) => {
|
pageRecords.forEach((item) => {
|
||||||
allPageTasks.push(async () => {
|
allPageTasks.push(async () => {
|
||||||
const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc");
|
const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc");
|
||||||
|
|
||||||
if (!fileRes.success) {
|
if (!fileRes.success) {
|
||||||
console.error("获取附件失败:", item.b_container_no);
|
console.error("获取附件失败:", item.b_container_no);
|
||||||
return;
|
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 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 files = fileRes.data || [];
|
||||||
|
|
||||||
const fileTasks = files.map((file, idx) => async () => {
|
const fileTasks = files.map((file, idx) => async () => {
|
||||||
try {
|
try {
|
||||||
const blob = await getAliyunFileBlob(file.mx_mapfilename);
|
const blob = await getFileBlob(file.mx_mapfilename);
|
||||||
const fileName = buildFileName(item.b_container_no, idx + 1, file.mx_filename) || "未命名文件";
|
|
||||||
|
const fileName =
|
||||||
|
buildFileName(containerNo, idx + 1, file.mx_filename) || "未命名文件";
|
||||||
|
|
||||||
folder.file(fileName, blob);
|
folder.file(fileName, blob);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(item.b_container_no + "下载失败:", file.mx_mapfilename, e);
|
console.error(containerNo + "下载失败:", file.mx_mapfilename, e);
|
||||||
} finally {
|
} finally {
|
||||||
finished++;
|
finished++;
|
||||||
loading.setText(`正在下载 ${finished} 个文件...`);
|
loading.setText(`正在下载 ${finished} 个文件...`);
|
||||||
@@ -376,6 +459,7 @@ async function downloadChunk() {
|
|||||||
|
|
||||||
loading.setText("正在生成压缩包...");
|
loading.setText("正在生成压缩包...");
|
||||||
const content = await zip.generateAsync({ type: "blob" });
|
const content = await zip.generateAsync({ type: "blob" });
|
||||||
|
|
||||||
saveAs(content, buildZipName("CHUNK"));
|
saveAs(content, buildZipName("CHUNK"));
|
||||||
ElMessage.success("下载完成");
|
ElMessage.success("下载完成");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -385,7 +469,6 @@ async function downloadChunk() {
|
|||||||
loading.close();
|
loading.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFileName(containerNo, index, fileName) {
|
function buildFileName(containerNo, index, fileName) {
|
||||||
const formattedIndex = String(index).padStart(2, "0");
|
const formattedIndex = String(index).padStart(2, "0");
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
<script lang="js" setup>
|
<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 {
|
import {
|
||||||
deleteFileApi,
|
deleteFileApi,
|
||||||
loadDataApi,
|
loadDataApi,
|
||||||
@@ -6,16 +10,16 @@ import {
|
|||||||
loadViewColumnsApi,
|
loadViewColumnsApi,
|
||||||
saveObjectApi,
|
saveObjectApi,
|
||||||
} from "@/request/api";
|
} from "@/request/api";
|
||||||
import { getAlignByFormat, formatValueByFormat } from "@/utils/columns";
|
import { formatValueByFormat } from "@/utils/columns";
|
||||||
import { updateViewTableColumns } 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 { formatToDate } from "@/utils/time";
|
||||||
import { ElLoading, ElMessage } from "element-plus";
|
import { ElLoading, ElMessage } from "element-plus";
|
||||||
import { saveAs } from "file-saver";
|
import { saveAs } from "file-saver";
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
import { usePermissionStore } from "@/store/permission";
|
import { usePermissionStore } from "@/store/permission";
|
||||||
import { ExportVxeTableExcel, exportExcel } from "@/utils/excel";
|
import { ExportVxeTableExcel } from "@/utils/excel";
|
||||||
import HighDownloadDialog from "./high-download-dialog.vue";
|
import { buildPublicUrl } from "@/utils/file";
|
||||||
|
|
||||||
const viewName = "v_container";
|
const viewName = "v_container";
|
||||||
|
|
||||||
@@ -32,6 +36,7 @@ const loading = reactive({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const showHighDownload = ref(false);
|
const showHighDownload = ref(false);
|
||||||
|
const showUpdate = ref(false);
|
||||||
|
|
||||||
const searchForm = ref({
|
const searchForm = ref({
|
||||||
startTime: "",
|
startTime: "",
|
||||||
@@ -130,6 +135,14 @@ async function loadTableDatas() {
|
|||||||
orderField: "b_inputdatetime DESC",
|
orderField: "b_inputdatetime DESC",
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
if (res.success) {
|
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;
|
tableData.value = res.data.rows;
|
||||||
pagination.total = res.data.total;
|
pagination.total = res.data.total;
|
||||||
|
|
||||||
@@ -204,9 +217,7 @@ async function deleteRows(rows = []) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const file of fileRes.data) {
|
await Promise.all(fileRes.data.map((file) => deleteFileApi(file.subid)));
|
||||||
await deleteFileApi(file.subid);
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestData = [
|
const requestData = [
|
||||||
{
|
{
|
||||||
@@ -267,6 +278,12 @@ function handleFileShow(data) {
|
|||||||
showFile.value = true;
|
showFile.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleUpdateShow(data) {
|
||||||
|
currentRow.value = data;
|
||||||
|
|
||||||
|
showUpdate.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleDownloadOne(data) {
|
async function handleDownloadOne(data) {
|
||||||
const loading = ElLoading.service({
|
const loading = ElLoading.service({
|
||||||
lock: true,
|
lock: true,
|
||||||
@@ -342,29 +359,55 @@ async function handleDownloadBatch() {
|
|||||||
let finished = 0;
|
let finished = 0;
|
||||||
let totalFiles = 0;
|
let totalFiles = 0;
|
||||||
|
|
||||||
// 1. 收集所有下载任务
|
|
||||||
const allTasks = [];
|
const allTasks = [];
|
||||||
|
|
||||||
|
// ✅ 关键:箱号计数器
|
||||||
|
const containerCountMap = new Map();
|
||||||
|
|
||||||
for (const item of records) {
|
for (const item of records) {
|
||||||
const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc");
|
const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc");
|
||||||
|
|
||||||
const files = (fileRes.success && fileRes.data) || [];
|
const files = (fileRes.success && fileRes.data) || [];
|
||||||
totalFiles += files.length;
|
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);
|
let folder = zip.folder(ymdFolderName);
|
||||||
folder = folder ? folder.folder(containerFolderName) : zip.folder(containerFolderName);
|
folder = folder ? folder.folder(containerFolderName) : zip.folder(containerFolderName);
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// 收集任务
|
||||||
|
// =========================
|
||||||
files.forEach((file, idx) => {
|
files.forEach((file, idx) => {
|
||||||
allTasks.push(async () => {
|
allTasks.push(async () => {
|
||||||
try {
|
try {
|
||||||
const blob = await getAliyunFileBlob(file.mx_mapfilename);
|
const blob = await getFileBlob(file.mx_mapfilename);
|
||||||
const fileName =
|
|
||||||
buildFileName(item.b_container_no, idx + 1, file.mx_filename) || "未命名文件";
|
const fileName = buildFileName(containerNo, idx + 1, file.mx_filename) || "未命名文件";
|
||||||
|
|
||||||
folder.file(fileName, blob);
|
folder.file(fileName, blob);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(item.b_container_no + "下载失败:", file.mx_mapfilename, e);
|
console.error(containerNo + "下载失败:", file.mx_mapfilename, e);
|
||||||
} finally {
|
} finally {
|
||||||
finished++;
|
finished++;
|
||||||
loading.setText(`正在下载 ${finished}/${totalFiles} 个文件...`);
|
loading.setText(`正在下载 ${finished}/${totalFiles} 个文件...`);
|
||||||
@@ -373,23 +416,25 @@ async function handleDownloadBatch() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 并发执行下载任务,限制同时 3 个
|
// =========================
|
||||||
|
// 并发执行(限制3个)
|
||||||
|
// =========================
|
||||||
await runWithLimit(allTasks, 3);
|
await runWithLimit(allTasks, 3);
|
||||||
|
|
||||||
loading.setText("正在生成压缩包...");
|
loading.setText("正在生成压缩包...");
|
||||||
|
|
||||||
const content = await zip.generateAsync({ type: "blob" });
|
const content = await zip.generateAsync({ type: "blob" });
|
||||||
|
|
||||||
// 3. 保存压缩包
|
|
||||||
saveAs(content, "批量下载");
|
saveAs(content, "批量下载");
|
||||||
|
|
||||||
ElMessage.success("下载完成");
|
ElMessage.success("下载完成");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
ElMessage.error("下载失败");
|
||||||
} finally {
|
} finally {
|
||||||
loading.close();
|
loading.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFileName(containerNo, index, fileName) {
|
function buildFileName(containerNo, index, fileName) {
|
||||||
const formattedIndex = String(index).padStart(2, "0");
|
const formattedIndex = String(index).padStart(2, "0");
|
||||||
|
|
||||||
@@ -575,29 +620,59 @@ function buildFileName(containerNo, index, fileName) {
|
|||||||
</vxe-column>
|
</vxe-column>
|
||||||
<vxe-column width="220" align="center" fixed="right" title="操作">
|
<vxe-column width="220" align="center" fixed="right" title="操作">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button
|
<div style="display: flex; align-items: center; justify-content: center">
|
||||||
link
|
<el-button
|
||||||
type="primary"
|
link
|
||||||
@click="handleDownloadOne(row)"
|
type="primary"
|
||||||
:disabled="!permissionStore.hasActionPermission(1)"
|
@click="handleDownloadOne(row)"
|
||||||
>
|
:disabled="!permissionStore.hasActionPermission(1)"
|
||||||
<el-icon class="el-icon--left">
|
>
|
||||||
<G3Icon icon="lucide:cloud-download" />
|
<el-icon class="el-icon--left">
|
||||||
</el-icon>
|
<G3Icon icon="lucide:cloud-download" />
|
||||||
下载
|
</el-icon>
|
||||||
</el-button>
|
下载
|
||||||
<el-button
|
</el-button>
|
||||||
link
|
<el-button
|
||||||
type="primary"
|
link
|
||||||
@click="handleFileShow(row)"
|
type="primary"
|
||||||
:disabled="!permissionStore.hasActionPermission(3)"
|
@click="handleFileShow(row)"
|
||||||
>
|
:disabled="!permissionStore.hasActionPermission(3)"
|
||||||
<el-icon class="el-icon--left">
|
>
|
||||||
<G3Icon icon="lucide:file-image" />
|
<el-icon class="el-icon--left">
|
||||||
</el-icon>
|
<G3Icon icon="lucide:file-image" />
|
||||||
图片
|
</el-icon>
|
||||||
</el-button>
|
图片
|
||||||
<el-button
|
</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
|
link
|
||||||
type="danger"
|
type="danger"
|
||||||
@click="handleDeleteRow(row)"
|
@click="handleDeleteRow(row)"
|
||||||
@@ -607,7 +682,7 @@ function buildFileName(containerNo, index, fileName) {
|
|||||||
<G3Icon icon="lucide:trash-2" />
|
<G3Icon icon="lucide:trash-2" />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button> -->
|
||||||
</template>
|
</template>
|
||||||
</vxe-column>
|
</vxe-column>
|
||||||
</vxe-table>
|
</vxe-table>
|
||||||
@@ -634,14 +709,16 @@ function buildFileName(containerNo, index, fileName) {
|
|||||||
@close="handlePreviewClose"
|
@close="handlePreviewClose"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<G3File
|
<G3ContainerFile
|
||||||
v-model="showFile"
|
v-model="showFile"
|
||||||
:father="currentRow.b_id"
|
:father="currentRow.b_id"
|
||||||
module-id="b_container"
|
module-id="b_container"
|
||||||
:categories="fileCategories"
|
:categories="fileCategories"
|
||||||
|
:container-no="currentRow.b_container_no"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<HighDownloadDialog v-model="showHighDownload" />
|
<HighDownloadDialog v-model="showHighDownload" />
|
||||||
|
<UpdateDialog v-model="showUpdate" :data="currentRow" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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