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

@@ -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>