u
This commit is contained in:
1 parent
f9eaacf02a
commit
69eda8b8ad
27 files changed
+2797
-616
No files matched your search
@@ -10,6 +10,7 @@ PUBLIC_SERVER_PORT=8080
|
||||
|
||||
# API
|
||||
# 后端 API 基础地址
|
||||
PUBLIC_API_BASE_URL=http://ser369.g3soft.cn:80/api
|
||||
# PUBLIC_API_BASE_URL=http://ser369.g3soft.cn:80/api
|
||||
PUBLIC_API_BASE_URL=http://192.168.1.137:80/api
|
||||
|
||||
PUBLIC_ALIYUN_BUCKET = g3hd
|
||||
+1
@@ -39,6 +39,7 @@ declare module 'vue' {
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
G3Column: typeof import('./src/components/g3-table/g3-column.vue')['default']
|
||||
G3ContainerFile: typeof import('./src/components/g3-container-file/index.vue')['default']
|
||||
G3Detail: typeof import('./src/components/g3-detail/index.vue')['default']
|
||||
G3Error: typeof import('./src/components/g3-error/index.vue')['default']
|
||||
G3File: typeof import('./src/components/g3-file/index.vue')['default']
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -27,8 +27,15 @@ import {
|
||||
getAliyunFileBlob,
|
||||
getFileExtension,
|
||||
uploadFile,
|
||||
buildPublicUrl,
|
||||
} from "@/utils/file";
|
||||
import { loadDataApi, saveObjectKeysApi, deleteFileApi } from "@/request/api";
|
||||
import {
|
||||
loadDataApi,
|
||||
insertFileApi,
|
||||
saveObjectApi,
|
||||
deleteFileApi,
|
||||
uploadCommonFileApi,
|
||||
} from "@/request/api";
|
||||
|
||||
const props = defineProps({
|
||||
father: {
|
||||
@@ -158,7 +165,7 @@ async function handleConfirmUpdate() {
|
||||
];
|
||||
|
||||
try {
|
||||
const response = await saveObjectKeysApi(requestData);
|
||||
const response = await saveObjectApi(requestData);
|
||||
|
||||
if (response.success) {
|
||||
ElMessage.success("文件名更新成功");
|
||||
@@ -239,8 +246,15 @@ async function loadFiles() {
|
||||
const res = await loadDataApi("bf_file", searchCondition, "mx_xh");
|
||||
|
||||
if (res.success) {
|
||||
fileList.value = res.data;
|
||||
oldFileList.value = JSON.parse(JSON.stringify(res.data));
|
||||
let datas = res.data.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
mx_mapfilename: buildPublicUrl(item.mx_mapfilename),
|
||||
};
|
||||
});
|
||||
|
||||
fileList.value = datas;
|
||||
oldFileList.value = JSON.parse(JSON.stringify(datas));
|
||||
loading.value = false;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -588,45 +602,69 @@ async function deleteFiles(files) {
|
||||
});
|
||||
}
|
||||
|
||||
// 保存
|
||||
// 保存
|
||||
async function handleSave() {
|
||||
loading.value = true;
|
||||
loadingTip.value = "正在保存中 . . .";
|
||||
|
||||
const { insertdata, updatedata, deletedata, hasChanges } = compareAndClassifyData(
|
||||
fileList.value,
|
||||
oldFileList.value,
|
||||
"subid",
|
||||
["_content", "_status", "checked"],
|
||||
);
|
||||
try {
|
||||
const { insertdata, updatedata, deletedata, hasChanges } = compareAndClassifyData(
|
||||
fileList.value,
|
||||
oldFileList.value,
|
||||
"subid",
|
||||
["_content", "_status", "checked"],
|
||||
);
|
||||
|
||||
// 处理新增的文件
|
||||
for (const item of insertdata) {
|
||||
try {
|
||||
await uploadFile(
|
||||
item._content,
|
||||
item.mx_filename,
|
||||
item.mx_fileext,
|
||||
item.mx_filesize,
|
||||
item.mx_xh,
|
||||
props.moduleId,
|
||||
props.father,
|
||||
);
|
||||
console.log(`文件 ${item.mx_filename} 上传成功`);
|
||||
item._status = "";
|
||||
} catch (error) {
|
||||
console.error(`文件 ${item.mx_filename} 上传失败:`, error);
|
||||
ElMessage.error(`文件 ${item.mx_filename} 上传失败,请重试`);
|
||||
// 没有变化直接结束
|
||||
if (!hasChanges) {
|
||||
ElMessage.info("没有数据变更");
|
||||
return;
|
||||
}
|
||||
|
||||
const mxXh =
|
||||
fileList.value.length > 0 ? Math.max(...fileList.value.map((item) => item.mx_xh ?? 0)) : 2;
|
||||
|
||||
// 处理新增
|
||||
for (const item of insertdata) {
|
||||
try {
|
||||
const uploadRes = await uploadCommonFileApi(item.mx_mapfilename, {
|
||||
fileName: item.mx_filename,
|
||||
});
|
||||
|
||||
if (!uploadRes.success) {
|
||||
throw new Error("上传失败");
|
||||
}
|
||||
|
||||
const insertRes = await insertFileApi({
|
||||
father: props.father,
|
||||
mx_xh: mxXh + 1,
|
||||
mx_filename: item.mx_filename,
|
||||
mx_filesize: item.mx_filesize,
|
||||
mx_fileext: item.mx_fileext,
|
||||
mx_mapfilename: uploadRes.data.path,
|
||||
mx_moduleid: props.moduleId,
|
||||
mx_cate_id: props.cateId,
|
||||
});
|
||||
|
||||
if (!insertRes.success) {
|
||||
throw new Error("入库失败");
|
||||
}
|
||||
|
||||
console.log(`文件 ${item.mx_filename} 上传成功`);
|
||||
item._status = "";
|
||||
} catch (error) {
|
||||
console.error(`文件 ${item.mx_filename} 处理失败:`, error);
|
||||
ElMessage.error(`文件 ${item.mx_filename} 上传失败,请重试`);
|
||||
}
|
||||
}
|
||||
|
||||
ElMessage.success("保存成功");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loadFiles();
|
||||
}
|
||||
|
||||
ElMessage.success("上传成功");
|
||||
|
||||
loading.value = false;
|
||||
|
||||
loadFiles();
|
||||
}
|
||||
|
||||
async function handleDownloadZip() {
|
||||
if (checkedFiles.value.length < 2) {
|
||||
ElMessage.warning("打包文件数量不得少于2");
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { httpPost, httpUpload } from ".";
|
||||
|
||||
|
||||
|
||||
// 登录
|
||||
export function loginApi(orgid, userid, password) {
|
||||
return httpPost(`/auth/login`, {
|
||||
@@ -190,11 +188,31 @@ export function insertFileApi(params) {
|
||||
return httpPost("/file/insert", params);
|
||||
}
|
||||
|
||||
// export function deleteFileApi(subid) {
|
||||
// return httpPost("/file/delete", {
|
||||
// subid,
|
||||
// tableName: "bf_file",
|
||||
// bucketName: import.meta.env.PUBLIC_ALIYUN_BUCKET,
|
||||
// });
|
||||
// }
|
||||
|
||||
export function deleteFileApi(subid) {
|
||||
return httpPost("/file/delete", {
|
||||
subid,
|
||||
tableName: "bf_file",
|
||||
bucketName: import.meta.env.PUBLIC_ALIYUN_BUCKET,
|
||||
});
|
||||
}
|
||||
|
||||
export function uploadContainerFileApi(filePath, { jobId, containerNo, fileName }) {
|
||||
return httpUpload("/file/uploadContainer", filePath, {
|
||||
jobId,
|
||||
containerNo,
|
||||
fileName,
|
||||
});
|
||||
}
|
||||
|
||||
export function uploadCommonFileApi(filePath, { fileName }) {
|
||||
return httpUpload("/file/uploadCommon", filePath, {
|
||||
fileName,
|
||||
});
|
||||
}
|
||||
// ============================= file =============================
|
||||
@@ -120,19 +120,17 @@ export function httpPost(url, data, config) {
|
||||
* @param {Object} config - axios 配置(可选)
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export function httpUpload(url, file, extraData = {}, config = {}) {
|
||||
export async function httpUpload(url, file, extraData = {}, config = {}) {
|
||||
const formData = new FormData();
|
||||
|
||||
// 兼容单文件 & 多文件
|
||||
if (Array.isArray(file)) {
|
||||
file.forEach((f) => {
|
||||
formData.append("file", f);
|
||||
});
|
||||
for (const f of file) {
|
||||
await appendFile(formData, f);
|
||||
}
|
||||
} else {
|
||||
formData.append("file", file);
|
||||
await appendFile(formData, file);
|
||||
}
|
||||
|
||||
// 追加额外参数
|
||||
Object.keys(extraData).forEach((key) => {
|
||||
formData.append(key, extraData[key]);
|
||||
});
|
||||
@@ -147,6 +145,24 @@ export function httpUpload(url, file, extraData = {}, config = {}) {
|
||||
.then((res) => res.data);
|
||||
}
|
||||
|
||||
async function appendFile(formData, file) {
|
||||
// ✅ 已经是 File / Blob
|
||||
if (file instanceof File || file instanceof Blob) {
|
||||
formData.append("file", file);
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ 处理 blob:http://xxx
|
||||
if (typeof file === "string" && file.startsWith("blob:")) {
|
||||
const blob = await fetch(file).then((res) => res.blob());
|
||||
|
||||
formData.append("file", blob);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("不支持的文件类型:" + typeof file);
|
||||
}
|
||||
|
||||
export function reset401Flag() {
|
||||
hasShown401 = false;
|
||||
}
|
||||
|
||||
@@ -203,7 +203,8 @@ export function getFileExtension(fileName) {
|
||||
return fileExt;
|
||||
}
|
||||
|
||||
export function downloadZip(files, fileName = "文件合集", callback) {
|
||||
|
||||
export function downloadOssZip(files, fileName = "文件合集", callback) {
|
||||
const zip = new JSZip();
|
||||
const addedFiles = new Set();
|
||||
|
||||
@@ -415,3 +416,78 @@ export function getAliyunFileBlob(url) {
|
||||
}
|
||||
|
||||
// ============================================ 阿里云上传和下载 ============================================
|
||||
|
||||
// ============================================ 本地文件上传和下载 ==========================================
|
||||
export function buildPublicUrl(filePath) {
|
||||
const baseUrl = import.meta.env.PUBLIC_API_BASE_URL || "";
|
||||
|
||||
return joinUrl(baseUrl, "/static/file", filePath);
|
||||
}
|
||||
|
||||
export function getFileBlob(path) {
|
||||
const fullUrl = buildPublicUrl(path)
|
||||
|
||||
return fetch(fullUrl).then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`获取文件失败,状态码:${response.status}`);
|
||||
}
|
||||
return response.blob();
|
||||
});
|
||||
}
|
||||
|
||||
export function downloadZip(files, fileName = "文件合集", callback) {
|
||||
const zip = new JSZip();
|
||||
const addedFiles = new Set();
|
||||
|
||||
const tasks = files.map((item) => {
|
||||
return getFileBlob(item.mx_mapfilename)
|
||||
.then((res) => {
|
||||
const uniqueFileName = getUniqueFileName(item.mx_filename, addedFiles);
|
||||
zip.file(uniqueFileName, res, { binary: true });
|
||||
addedFiles.add(uniqueFileName);
|
||||
})
|
||||
.catch((err) => {
|
||||
// 捕获单个文件下载失败的错误,不影响其他文件下载
|
||||
console.error(`Failed to download file: ${item.mx_filename}`, err);
|
||||
});
|
||||
});
|
||||
|
||||
// 关键:使用 Promise.all 确保所有任务完成
|
||||
return Promise.all(tasks)
|
||||
.then(() => zip.generateAsync({ type: "blob" }))
|
||||
.then((res) => {
|
||||
saveAs(res, `${fileName}.zip`);
|
||||
callback && callback();
|
||||
})
|
||||
.catch((err) => {
|
||||
callback && callback();
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
// ============================================ 本地文件上传和下载 ==========================================
|
||||
|
||||
export function joinUrl(...parts) {
|
||||
if (!parts || parts.length === 0) return "";
|
||||
|
||||
const sb = [];
|
||||
|
||||
for (let part of parts) {
|
||||
if (!part) continue;
|
||||
|
||||
part = String(part).trim();
|
||||
if (!part) continue;
|
||||
|
||||
// 替换 Windows 路径分隔符
|
||||
part = part.replace(/\\/g, "/");
|
||||
|
||||
if (sb.length === 0) {
|
||||
// 第一段:保留协议(http:// https://),去掉末尾 /
|
||||
sb.push(part.replace(/\/+$/, ""));
|
||||
} else {
|
||||
// 后续段:去掉前后 /
|
||||
sb.push(part.replace(/^\/+|\/+$/g, ""));
|
||||
}
|
||||
}
|
||||
|
||||
return sb.join("/");
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { useThrottle } from "@/utils/throttle";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { saveAs } from "file-saver";
|
||||
import JSZip from "jszip";
|
||||
import { getAliyunFileBlob } from "@/utils/file";
|
||||
import { getFileBlob } from "@/utils/file";
|
||||
import { formatToDate } from "@/utils/time";
|
||||
|
||||
const viewName = "v_container";
|
||||
@@ -189,28 +189,55 @@ async function downloadAll() {
|
||||
let finished = 0;
|
||||
let totalFiles = 0;
|
||||
|
||||
// 1. 收集所有下载任务
|
||||
const allTasks = [];
|
||||
|
||||
// ✅ 关键:箱号计数器
|
||||
const containerCountMap = new Map();
|
||||
|
||||
for (const item of records) {
|
||||
const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc");
|
||||
|
||||
const files = (fileRes.success && fileRes.data) || [];
|
||||
totalFiles += files.length;
|
||||
|
||||
// 按 年/月/日/集装箱号 生成层级文件夹
|
||||
const ymdFolderName = formatToDate(item.b_inputdatetime, "YYYY/MM/DD"); // 2026/03/27
|
||||
const containerFolderName = item.b_container_no;
|
||||
let folder = zip.folder(ymdFolderName);
|
||||
folder = folder ? folder.folder(containerFolderName) : zip.folder(containerFolderName);
|
||||
// =========================
|
||||
// ✅ 生成“箱号文件夹名称”
|
||||
// =========================
|
||||
const containerNo = item.b_container_no || "未知箱号";
|
||||
|
||||
const count = containerCountMap.get(containerNo) || 0;
|
||||
|
||||
let folderName = containerNo;
|
||||
|
||||
if (count === 1) {
|
||||
folderName = `${containerNo}_01`;
|
||||
} else if (count > 1) {
|
||||
folderName = `${containerNo}_${String(count - 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
containerCountMap.set(containerNo, count + 1);
|
||||
|
||||
// =========================
|
||||
// 生成日期目录
|
||||
// =========================
|
||||
const ymdFolderName = formatToDate(item.b_inputdatetime, "YYYY/MM/DD");
|
||||
|
||||
let folder = zip.folder(ymdFolderName);
|
||||
folder = folder ? folder.folder(folderName) : zip.folder(folderName);
|
||||
|
||||
// =========================
|
||||
// 收集下载任务
|
||||
// =========================
|
||||
files.forEach((file, idx) => {
|
||||
allTasks.push(async () => {
|
||||
try {
|
||||
const blob = await getAliyunFileBlob(file.mx_mapfilename);
|
||||
const fileName = buildFileName(item.b_container_no, idx + 1, file.mx_filename) || "未命名文件";
|
||||
const blob = await getFileBlob(file.mx_mapfilename);
|
||||
|
||||
const fileName = buildFileName(containerNo, idx + 1, file.mx_filename) || "未命名文件";
|
||||
|
||||
folder.file(fileName, blob);
|
||||
} catch (e) {
|
||||
console.error(item.b_container_no + "下载失败:", file.mx_mapfilename, e);
|
||||
console.error(`${containerNo} 下载失败:`, file.mx_mapfilename, e);
|
||||
} finally {
|
||||
finished++;
|
||||
loading.setText(`正在下载 ${finished}/${totalFiles} 个文件...`);
|
||||
@@ -219,18 +246,21 @@ async function downloadAll() {
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 并发执行下载任务,限制同时 3 个
|
||||
// =========================
|
||||
// 并发执行(限制3个)
|
||||
// =========================
|
||||
await runWithLimit(allTasks, 3);
|
||||
|
||||
loading.setText("正在生成压缩包...");
|
||||
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
|
||||
// 3. 保存压缩包
|
||||
saveAs(content, buildZipName("ALL"));
|
||||
|
||||
ElMessage.success("下载完成");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ElMessage.error("下载失败");
|
||||
} finally {
|
||||
loading.close();
|
||||
}
|
||||
@@ -246,6 +276,7 @@ async function downloadSlice() {
|
||||
let start = Math.max(1, Math.min(sliceModeForm.value.startPage || 1, allTotal.value));
|
||||
let end = Math.max(1, Math.min(sliceModeForm.value.endPage || 1, allTotal.value));
|
||||
if (start > end) [start, end] = [end, start];
|
||||
|
||||
sliceModeForm.value.startPage = start;
|
||||
sliceModeForm.value.endPage = end;
|
||||
|
||||
@@ -268,28 +299,51 @@ async function downloadSlice() {
|
||||
let finished = 0;
|
||||
const allTasks = [];
|
||||
|
||||
// ✅ 箱号计数
|
||||
const containerCountMap = new Map();
|
||||
|
||||
records.forEach((item) => {
|
||||
const ymdFolderName = formatToDate(item.b_inputdatetime, "YYYY/MM/DD");
|
||||
const containerFolderName = item.b_container_no;
|
||||
|
||||
const containerNo = item.b_container_no || "未知箱号";
|
||||
|
||||
// ===== 关键:生成文件夹名 =====
|
||||
const count = containerCountMap.get(containerNo) || 0;
|
||||
|
||||
let containerFolderName = containerNo;
|
||||
|
||||
if (count === 1) {
|
||||
containerFolderName = `${containerNo}_01`;
|
||||
} else if (count > 1) {
|
||||
containerFolderName = `${containerNo}_${String(count - 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
containerCountMap.set(containerNo, count + 1);
|
||||
|
||||
const folderPromise = loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc");
|
||||
|
||||
allTasks.push(async () => {
|
||||
const fileRes = await folderPromise;
|
||||
|
||||
if (!fileRes.success) {
|
||||
console.error("获取附件失败:", item.b_container_no);
|
||||
console.error("获取附件失败:", containerNo);
|
||||
return;
|
||||
}
|
||||
|
||||
const folder = zip.folder(ymdFolderName)?.folder(containerFolderName) || zip.folder(containerFolderName);
|
||||
let folder = zip.folder(ymdFolderName);
|
||||
folder = folder ? folder.folder(containerFolderName) : zip.folder(containerFolderName);
|
||||
|
||||
const files = fileRes.data || [];
|
||||
|
||||
const fileTasks = files.map((file, idx) => async () => {
|
||||
try {
|
||||
const blob = await getAliyunFileBlob(file.mx_mapfilename);
|
||||
const fileName = buildFileName(item.b_container_no, idx + 1, file.mx_filename) || "未命名文件";
|
||||
const blob = await getFileBlob(file.mx_mapfilename);
|
||||
|
||||
const fileName = buildFileName(containerNo, idx + 1, file.mx_filename) || "未命名文件";
|
||||
|
||||
folder.file(fileName, blob);
|
||||
} catch (e) {
|
||||
console.error(item.b_container_no + "下载失败:", file.mx_mapfilename, e);
|
||||
console.error(containerNo + "下载失败:", file.mx_mapfilename, e);
|
||||
} finally {
|
||||
finished++;
|
||||
loading.setText(`正在下载 ${finished} / ${records.length} 个文件...`);
|
||||
@@ -304,6 +358,7 @@ async function downloadSlice() {
|
||||
|
||||
loading.setText("正在生成压缩包...");
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
|
||||
saveAs(content, buildZipName("SLICE"));
|
||||
ElMessage.success("下载完成");
|
||||
} catch (e) {
|
||||
@@ -322,6 +377,7 @@ async function downloadChunk() {
|
||||
|
||||
try {
|
||||
const records = await fetchDatas();
|
||||
|
||||
if (!records?.length) {
|
||||
ElMessage.warning("无有效数据");
|
||||
return;
|
||||
@@ -334,33 +390,60 @@ async function downloadChunk() {
|
||||
|
||||
const allPageTasks = [];
|
||||
|
||||
// ✅ 箱号计数(关键)
|
||||
const containerCountMap = new Map();
|
||||
|
||||
for (let pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||
const startIndex = pageIndex * pageSize;
|
||||
const endIndex = Math.min((pageIndex + 1) * pageSize, records.length);
|
||||
const pageRecords = records.slice(startIndex, endIndex);
|
||||
|
||||
const pageFolderName = `${startIndex + 1}-${endIndex}`;
|
||||
const pageFolder = zip.folder(pageFolderName);
|
||||
|
||||
pageRecords.forEach((item) => {
|
||||
allPageTasks.push(async () => {
|
||||
const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc");
|
||||
|
||||
if (!fileRes.success) {
|
||||
console.error("获取附件失败:", item.b_container_no);
|
||||
return;
|
||||
}
|
||||
|
||||
const containerNo = item.b_container_no || "未知箱号";
|
||||
|
||||
// ===== 生成箱号文件夹名 =====
|
||||
const count = containerCountMap.get(containerNo) || 0;
|
||||
|
||||
let containerFolderName = containerNo;
|
||||
|
||||
if (count === 1) {
|
||||
containerFolderName = `${containerNo}_01`;
|
||||
} else if (count > 1) {
|
||||
containerFolderName = `${containerNo}_${String(count - 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
containerCountMap.set(containerNo, count + 1);
|
||||
|
||||
const ymdFolderName = formatToDate(item.b_inputdatetime, "YYYY/MM/DD");
|
||||
const containerFolderName = item.b_container_no;
|
||||
const folder = pageFolder.folder(ymdFolderName)?.folder(containerFolderName) || pageFolder.folder(containerFolderName);
|
||||
|
||||
let folder = pageFolder.folder(ymdFolderName);
|
||||
folder = folder
|
||||
? folder.folder(containerFolderName)
|
||||
: pageFolder.folder(containerFolderName);
|
||||
|
||||
const files = fileRes.data || [];
|
||||
|
||||
const fileTasks = files.map((file, idx) => async () => {
|
||||
try {
|
||||
const blob = await getAliyunFileBlob(file.mx_mapfilename);
|
||||
const fileName = buildFileName(item.b_container_no, idx + 1, file.mx_filename) || "未命名文件";
|
||||
const blob = await getFileBlob(file.mx_mapfilename);
|
||||
|
||||
const fileName =
|
||||
buildFileName(containerNo, idx + 1, file.mx_filename) || "未命名文件";
|
||||
|
||||
folder.file(fileName, blob);
|
||||
} catch (e) {
|
||||
console.error(item.b_container_no + "下载失败:", file.mx_mapfilename, e);
|
||||
console.error(containerNo + "下载失败:", file.mx_mapfilename, e);
|
||||
} finally {
|
||||
finished++;
|
||||
loading.setText(`正在下载 ${finished} 个文件...`);
|
||||
@@ -376,6 +459,7 @@ async function downloadChunk() {
|
||||
|
||||
loading.setText("正在生成压缩包...");
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
|
||||
saveAs(content, buildZipName("CHUNK"));
|
||||
ElMessage.success("下载完成");
|
||||
} catch (e) {
|
||||
@@ -385,7 +469,6 @@ async function downloadChunk() {
|
||||
loading.close();
|
||||
}
|
||||
}
|
||||
|
||||
function buildFileName(containerNo, index, fileName) {
|
||||
const formattedIndex = String(index).padStart(2, "0");
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<script lang="js" setup>
|
||||
import G3ContainerFile from "@/components/g3-container-file/index.vue";
|
||||
import HighDownloadDialog from "./high-download-dialog.vue";
|
||||
import UpdateDialog from "./update-dialog.vue";
|
||||
|
||||
import {
|
||||
deleteFileApi,
|
||||
loadDataApi,
|
||||
@@ -6,16 +10,16 @@ import {
|
||||
loadViewColumnsApi,
|
||||
saveObjectApi,
|
||||
} from "@/request/api";
|
||||
import { getAlignByFormat, formatValueByFormat } from "@/utils/columns";
|
||||
import { formatValueByFormat } from "@/utils/columns";
|
||||
import { updateViewTableColumns } from "@/utils/columns";
|
||||
import { downloadZip, getAliyunFileBlob } from "@/utils/file";
|
||||
import { downloadZip, getFileBlob } from "@/utils/file";
|
||||
import { formatToDate } from "@/utils/time";
|
||||
import { ElLoading, ElMessage } from "element-plus";
|
||||
import { saveAs } from "file-saver";
|
||||
import JSZip from "jszip";
|
||||
import { usePermissionStore } from "@/store/permission";
|
||||
import { ExportVxeTableExcel, exportExcel } from "@/utils/excel";
|
||||
import HighDownloadDialog from "./high-download-dialog.vue";
|
||||
import { ExportVxeTableExcel } from "@/utils/excel";
|
||||
import { buildPublicUrl } from "@/utils/file";
|
||||
|
||||
const viewName = "v_container";
|
||||
|
||||
@@ -32,6 +36,7 @@ const loading = reactive({
|
||||
});
|
||||
|
||||
const showHighDownload = ref(false);
|
||||
const showUpdate = ref(false);
|
||||
|
||||
const searchForm = ref({
|
||||
startTime: "",
|
||||
@@ -130,6 +135,14 @@ async function loadTableDatas() {
|
||||
orderField: "b_inputdatetime DESC",
|
||||
}).then((res) => {
|
||||
if (res.success) {
|
||||
console.log(res.data.rows);
|
||||
|
||||
res.data.rows.forEach((row) => {
|
||||
if (row.b_container_img) {
|
||||
row.b_container_img = buildPublicUrl(row.b_container_img);
|
||||
}
|
||||
});
|
||||
|
||||
tableData.value = res.data.rows;
|
||||
pagination.total = res.data.total;
|
||||
|
||||
@@ -204,9 +217,7 @@ async function deleteRows(rows = []) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of fileRes.data) {
|
||||
await deleteFileApi(file.subid);
|
||||
}
|
||||
await Promise.all(fileRes.data.map((file) => deleteFileApi(file.subid)));
|
||||
|
||||
const requestData = [
|
||||
{
|
||||
@@ -267,6 +278,12 @@ function handleFileShow(data) {
|
||||
showFile.value = true;
|
||||
}
|
||||
|
||||
function handleUpdateShow(data) {
|
||||
currentRow.value = data;
|
||||
|
||||
showUpdate.value = true;
|
||||
}
|
||||
|
||||
async function handleDownloadOne(data) {
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
@@ -342,29 +359,55 @@ async function handleDownloadBatch() {
|
||||
let finished = 0;
|
||||
let totalFiles = 0;
|
||||
|
||||
// 1. 收集所有下载任务
|
||||
const allTasks = [];
|
||||
|
||||
// ✅ 关键:箱号计数器
|
||||
const containerCountMap = new Map();
|
||||
|
||||
for (const item of records) {
|
||||
const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc");
|
||||
|
||||
const files = (fileRes.success && fileRes.data) || [];
|
||||
totalFiles += files.length;
|
||||
|
||||
// 按 年/月/日/集装箱号 生成层级文件夹
|
||||
const ymdFolderName = formatToDate(item.b_inputdatetime, "YYYY/MM/DD"); // 2026/03/27
|
||||
const containerFolderName = item.b_container_no;
|
||||
// =========================
|
||||
// ✅ 箱号文件夹去重逻辑
|
||||
// =========================
|
||||
const containerNo = item.b_container_no || "未知箱号";
|
||||
|
||||
const count = containerCountMap.get(containerNo) || 0;
|
||||
|
||||
let containerFolderName = containerNo;
|
||||
|
||||
if (count === 1) {
|
||||
containerFolderName = `${containerNo}_01`;
|
||||
} else if (count > 1) {
|
||||
containerFolderName = `${containerNo}_${String(count - 1).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
containerCountMap.set(containerNo, count + 1);
|
||||
|
||||
// =========================
|
||||
// 日期文件夹
|
||||
// =========================
|
||||
const ymdFolderName = formatToDate(item.b_inputdatetime, "YYYY/MM/DD");
|
||||
|
||||
let folder = zip.folder(ymdFolderName);
|
||||
folder = folder ? folder.folder(containerFolderName) : zip.folder(containerFolderName);
|
||||
|
||||
// =========================
|
||||
// 收集任务
|
||||
// =========================
|
||||
files.forEach((file, idx) => {
|
||||
allTasks.push(async () => {
|
||||
try {
|
||||
const blob = await getAliyunFileBlob(file.mx_mapfilename);
|
||||
const fileName =
|
||||
buildFileName(item.b_container_no, idx + 1, file.mx_filename) || "未命名文件";
|
||||
const blob = await getFileBlob(file.mx_mapfilename);
|
||||
|
||||
const fileName = buildFileName(containerNo, idx + 1, file.mx_filename) || "未命名文件";
|
||||
|
||||
folder.file(fileName, blob);
|
||||
} catch (e) {
|
||||
console.error(item.b_container_no + "下载失败:", file.mx_mapfilename, e);
|
||||
console.error(containerNo + "下载失败:", file.mx_mapfilename, e);
|
||||
} finally {
|
||||
finished++;
|
||||
loading.setText(`正在下载 ${finished}/${totalFiles} 个文件...`);
|
||||
@@ -373,23 +416,25 @@ async function handleDownloadBatch() {
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 并发执行下载任务,限制同时 3 个
|
||||
// =========================
|
||||
// 并发执行(限制3个)
|
||||
// =========================
|
||||
await runWithLimit(allTasks, 3);
|
||||
|
||||
loading.setText("正在生成压缩包...");
|
||||
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
|
||||
// 3. 保存压缩包
|
||||
saveAs(content, "批量下载");
|
||||
|
||||
ElMessage.success("下载完成");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ElMessage.error("下载失败");
|
||||
} finally {
|
||||
loading.close();
|
||||
}
|
||||
}
|
||||
|
||||
function buildFileName(containerNo, index, fileName) {
|
||||
const formattedIndex = String(index).padStart(2, "0");
|
||||
|
||||
@@ -575,29 +620,59 @@ function buildFileName(containerNo, index, fileName) {
|
||||
</vxe-column>
|
||||
<vxe-column width="220" align="center" fixed="right" title="操作">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleDownloadOne(row)"
|
||||
:disabled="!permissionStore.hasActionPermission(1)"
|
||||
>
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:cloud-download" />
|
||||
</el-icon>
|
||||
下载
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleFileShow(row)"
|
||||
:disabled="!permissionStore.hasActionPermission(3)"
|
||||
>
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:file-image" />
|
||||
</el-icon>
|
||||
图片
|
||||
</el-button>
|
||||
<el-button
|
||||
<div style="display: flex; align-items: center; justify-content: center">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleDownloadOne(row)"
|
||||
:disabled="!permissionStore.hasActionPermission(1)"
|
||||
>
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:cloud-download" />
|
||||
</el-icon>
|
||||
下载
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleFileShow(row)"
|
||||
:disabled="!permissionStore.hasActionPermission(3)"
|
||||
>
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:file-image" />
|
||||
</el-icon>
|
||||
图片
|
||||
</el-button>
|
||||
<el-dropdown>
|
||||
<el-button link type="primary" style="margin-left: 8px">
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:circle-ellipsis" />
|
||||
</el-icon>
|
||||
更多
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="handleUpdateShow(row)">
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:refresh-ccw" />
|
||||
</el-icon>
|
||||
更新
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
@click="handleDeleteRow(row)"
|
||||
:disabled="!permissionStore.hasActionPermission(2)"
|
||||
>
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:trash-2" />
|
||||
</el-icon>
|
||||
删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
<!-- <el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDeleteRow(row)"
|
||||
@@ -607,7 +682,7 @@ function buildFileName(containerNo, index, fileName) {
|
||||
<G3Icon icon="lucide:trash-2" />
|
||||
</el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</el-button> -->
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
@@ -634,14 +709,16 @@ function buildFileName(containerNo, index, fileName) {
|
||||
@close="handlePreviewClose"
|
||||
/>
|
||||
|
||||
<G3File
|
||||
<G3ContainerFile
|
||||
v-model="showFile"
|
||||
:father="currentRow.b_id"
|
||||
module-id="b_container"
|
||||
:categories="fileCategories"
|
||||
:container-no="currentRow.b_container_no"
|
||||
/>
|
||||
|
||||
<HighDownloadDialog v-model="showHighDownload" />
|
||||
<UpdateDialog v-model="showUpdate" :data="currentRow" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<script lang="js" setup>
|
||||
const visible = defineModel(false);
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const loading = reactive({
|
||||
submit: false,
|
||||
global: true,
|
||||
});
|
||||
|
||||
const form = ref({
|
||||
b_id: "",
|
||||
b_container_no: "",
|
||||
});
|
||||
|
||||
const title = computed(() => {
|
||||
if (visible.value) {
|
||||
if (props.data.b_container_no) {
|
||||
return `更新 ${props.data.b_container_no}`;
|
||||
} else {
|
||||
return "更新";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function onOpened() {
|
||||
loading.global = true;
|
||||
console.log("对话框已打开");
|
||||
}
|
||||
|
||||
function onClosed() {
|
||||
console.log("对话框已关闭");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
v-bind="$attrs"
|
||||
:title="title"
|
||||
width="500"
|
||||
@opened="onOpened"
|
||||
@closed="onClosed"
|
||||
v-model="visible"
|
||||
>
|
||||
<div class="form-container" v-loading="loading.global">
|
||||
<!-- 箱号 -->
|
||||
<div class="form-item">
|
||||
<div class="label">箱号</div>
|
||||
<div class="content">
|
||||
<el-input v-model="form.b_container_no" placeholder="请输入箱号" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button :loading="loading.submit" @click="visible = false">
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:refresh-ccw" />
|
||||
</el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button type="primary">
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:cloud-download" />
|
||||
</el-icon>
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
::deep(.el-input) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user