This commit is contained in:
oneao committed 2026-03-30 17:27:19 +08:00
1 parent f9eaacf02a
commit 69eda8b8ad
27 files changed
+2797 -616

No files matched your search

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