-
-
技术原理
-
这个演示通过以下步骤实现拖拽触发下载:
-
- - 为可拖拽元素添加
draggable="true" 属性
- - 监听
dragstart 事件
- - 在事件处理函数中,动态创建隐藏的下载链接
- - 设置链接的
href 和 download 属性
- - 通过 JavaScript 模拟点击链接,触发下载
-
-
-
-
-const draggable = document.getElementById('draggableElement');
-
-
-draggable.addEventListener('dragstart', function(e) {
-
- e.dataTransfer.effectAllowed = 'copy';
-
- e.dataTransfer.setData('text/plain', '下载文件');
-
-
- const link = document.createElement('a');
-
-
- const fileType = getSelectedFileType();
- link.href = getFileUrl(fileType);
- link.download = getFileName(fileType); // 下载的文件名
-
-
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
-
-
- showStatus('下载已开始,请检查浏览器的下载列表', 'success');
-});
-
-
+
+
-
-
技术限制与注意事项
-
- - 拖拽操作实际上触发的是浏览器的标准下载流程,文件会保存到默认下载目录,而不是拖拽的目标位置
- - 某些浏览器可能对拖拽触发下载有安全限制
- - 下载行为受浏览器下载设置的影响(如下载前询问保存位置)
- - 需要服务器正确设置CORS头部(如果文件来自不同源)
- - 对于大文件,下载可能在后台进行,用户可能不会立即察觉
-
+
+
-
+
-
- // 拖拽结束事件
- draggableElement.addEventListener('dragend', function () {
- this.classList.remove('drag-target');
- });
-
- // 更新拖拽元素的提示文本
- function updateDraggableText() {
- const fileTypeNames = {
- 'txt': '文本文件',
- 'png': '图片文件',
- 'pdf': 'PDF文档'
- };
-
- const fileTypeElement = draggableElement.querySelector('p');
- if (fileTypeElement) {
- fileTypeElement.textContent = `下次拖拽将下载: ${getFileName(currentFileType)} (${fileTypeNames[currentFileType]})`;
- }
- }
-
- // 初始化文本
- updateDraggableText();
-
- // 添加文件类型选择功能
- const fileItems = document.querySelectorAll('.file-item');
- fileItems.forEach((item, index) => {
- item.addEventListener('click', function () {
- const types = ['txt', 'png', 'pdf'];
- currentFileType = types[index];
- updateDraggableText();
-
- // 高亮显示选中的文件
- fileItems.forEach(el => el.style.backgroundColor = '#f8f9fa');
- this.style.backgroundColor = '#e3f2fd';
-
- showStatus(`已选择 "${getFileName(currentFileType)}",现在可以拖拽下载`, 'success');
- });
- });
-
- // 默认选中第一个文件
- if (fileItems.length > 0) {
- fileItems[0].style.backgroundColor = '#e3f2fd';
- }
- });
-
-
\ No newline at end of file
diff --git a/code/container-ocr/ocr-vue/components.d.ts b/code/container-ocr/ocr-vue/components.d.ts
index 7859f0d7..cfdd1710 100644
--- a/code/container-ocr/ocr-vue/components.d.ts
+++ b/code/container-ocr/ocr-vue/components.d.ts
@@ -13,6 +13,7 @@ declare module 'vue' {
export interface GlobalComponents {
Directory: typeof import('./src/components/wang-editor/modules/directory.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton']
+ ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
@@ -27,9 +28,12 @@ declare module 'vue' {
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElPagination: typeof import('element-plus/es')['ElPagination']
+ ElPopover: typeof import('element-plus/es')['ElPopover']
+ ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSplitter: typeof import('element-plus/es')['ElSplitter']
ElSplitterPanel: typeof import('element-plus/es')['ElSplitterPanel']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
+ ElSwitch: typeof import('element-plus/es')['ElSwitch']
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']
diff --git a/code/container-ocr/ocr-vue/src/styles/element-plus.scss b/code/container-ocr/ocr-vue/src/styles/element-plus.scss
index adc103e2..2a19075c 100644
--- a/code/container-ocr/ocr-vue/src/styles/element-plus.scss
+++ b/code/container-ocr/ocr-vue/src/styles/element-plus.scss
@@ -19,3 +19,14 @@
.el-button--large {
height: 32px;
}
+
+.el-loading-mask {
+ &.is-fullscreen {
+ .el-loading-spinner .path {
+ stroke: #fff !important;
+ }
+ .el-loading-text {
+ color: #fff;
+ }
+ }
+}
diff --git a/code/container-ocr/ocr-vue/src/utils/file.js b/code/container-ocr/ocr-vue/src/utils/file.js
index 0a159fcb..690bc091 100644
--- a/code/container-ocr/ocr-vue/src/utils/file.js
+++ b/code/container-ocr/ocr-vue/src/utils/file.js
@@ -287,7 +287,6 @@ export function getAliyunFileBlob(url) {
// 检查响应状态
if (!response.ok) {
- ElMessage.error(`下载失败:${response.status}`);
reject(new Error(`下载失败:${response.status}`));
return;
}
@@ -298,8 +297,6 @@ export function getAliyunFileBlob(url) {
// 返回blob
resolve(blob);
} catch (error) {
- // 捕获网络请求失败或其他错误
- ElMessage.error("下载文件时发生错误");
reject(error);
}
}
@@ -311,27 +308,26 @@ export function getAliyunFileBlob(url) {
export function downloadZip(files, fileName = "文件合集", callback) {
const zip = new JSZip();
- const addedFiles = new Set(); // 用来存储已添加的文件名
+ const addedFiles = new Set();
- let result = files.map((item) => {
- let promise = getAliyunFileBlob(item.mx_mapfilename).then((res) => {
- // 处理文件名重复的情况
- let uniqueFileName = getUniqueFileName(item.mx_filename, addedFiles);
+ const tasks = files.map((item) => {
+ return getAliyunFileBlob(item.mx_mapfilename).then((res) => {
+ const uniqueFileName = getUniqueFileName(item.mx_filename, addedFiles);
zip.file(uniqueFileName, res, { binary: true });
- addedFiles.add(uniqueFileName); // 添加文件名到已添加的集合中
+ addedFiles.add(uniqueFileName);
});
- return promise;
});
- Promise.all(result)
- .then(() => {
- zip.generateAsync({ type: "blob" }).then((res) => {
- callback && callback();
- saveAs(res, `${fileName}.zip`);
- });
- })
- .catch(() => {
+ // ✅ 关键:return
+ return Promise.all(tasks)
+ .then(() => zip.generateAsync({ type: "blob" }))
+ .then((res) => {
+ saveAs(res, `${fileName}.zip`);
callback && callback();
+ })
+ .catch((err) => {
+ callback && callback();
+ throw err; // ✅ 继续往外抛
});
}
diff --git a/code/container-ocr/ocr-vue/src/views/container/index.vue b/code/container-ocr/ocr-vue/src/views/container/index.vue
index 8883901a..63d25d98 100644
--- a/code/container-ocr/ocr-vue/src/views/container/index.vue
+++ b/code/container-ocr/ocr-vue/src/views/container/index.vue
@@ -8,7 +8,11 @@ import {
} from "@/request/api";
import { getAlignByFormat, formatValueByFormat } from "@/utils/columns";
import { updateViewTableColumns } from "@/utils/columns";
-import { ElMessage } from "element-plus";
+import { downloadZip } from "@/utils/file";
+import { formatToDate } from "@/utils/time";
+import { ElLoading, ElMessage } from "element-plus";
+import { saveAs } from "file-saver";
+import JSZip from "jszip";
const tableRef = ref();
@@ -49,7 +53,9 @@ const viewName = "v_container";
const showPreview = ref(false);
const previewIndex = ref(-1);
const previewImageList = computed(() => {
- return tableData.value.map((item) => item.b_container_img).filter((img) => img);
+ return tableData.value
+ .map((item) => item.b_container_img)
+ .filter((img) => img);
});
const currentRow = ref({});
@@ -172,38 +178,53 @@ async function deleteRows(rows = []) {
return;
}
- // 确保 deleteIds 数组中的值是数字,防止 SQL 注入
- const safeDeleteIds = deleteIds.map((id) => Number(id));
-
- // 使用安全的 SQL 查询
- const fileRes = await loadDataApi("bf_file", `father IN (${safeDeleteIds.join(",")})`, "", [
- "subid",
- ]);
-
- if (!fileRes.success) {
- return;
- }
-
- for (const file of fileRes.data) {
- await deleteFileApi(file.subid);
- }
-
- const requestData = [
- {
- tableName: "b_container",
- keyField: "b_id",
- inserts: [],
- updates: [],
- deletes: deleteIds.map((id) => ({ b_id: id })),
- },
- ];
-
- await saveObjectApi(requestData).then((res) => {
- if (res.success) {
- ElMessage.success("删除成功");
- loadTableDatas();
- }
+ const loading = ElLoading.service({
+ lock: true,
+ text: "正在删除中...",
});
+
+ try {
+ // 确保 deleteIds 数组中的值是数字,防止 SQL 注入
+ const safeDeleteIds = deleteIds.map((id) => Number(id));
+
+ // 使用安全的 SQL 查询
+ const fileRes = await loadDataApi(
+ "bf_file",
+ `father IN (${safeDeleteIds.join(",")})`,
+ "",
+ ["subid"],
+ );
+
+ if (!fileRes.success) {
+ return;
+ }
+
+ for (const file of fileRes.data) {
+ await deleteFileApi(file.subid);
+ }
+
+ const requestData = [
+ {
+ tableName: "b_container",
+ keyField: "b_id",
+ inserts: [],
+ updates: [],
+ deletes: deleteIds.map((id) => ({ b_id: id })),
+ },
+ ];
+
+ await saveObjectApi(requestData).then((res) => {
+ if (res.success) {
+ ElMessage.success("删除成功");
+ loadTableDatas();
+ }
+ });
+ } catch (err) {
+ ElMessage.error("删除失败");
+ console.log(err);
+ } finally {
+ loading.close();
+ }
}
function handleSearch() {
@@ -241,9 +262,167 @@ function handleFileShow(data) {
showFile.value = true;
}
-function handleDownloadOne(data) {
- console.log(data);
- // 获取
+async function handleDownloadOne(data) {
+ const loading = ElLoading.service({
+ lock: true,
+ text: "正在下载并打包文件,请稍候...",
+ });
+
+ try {
+ const fileRes = await loadDataApi(
+ "bf_file",
+ `father = '${data.b_id}'`,
+ "mx_xh asc",
+ );
+
+ if (!fileRes.success) {
+ ElMessage.error("获取附件失败");
+ return;
+ }
+
+ const downloadFiles = fileRes.data.map((item, index) => ({
+ mx_filename: buildFileName(
+ data.b_container_no,
+ index + 1,
+ item.mx_filename,
+ ),
+ mx_mapfilename: item.mx_mapfilename,
+ }));
+
+ await downloadZip(
+ downloadFiles,
+ `${data.b_container_no}_${formatToDate(data.b_inputdatetime)}`,
+ );
+
+ ElMessage.success("下载完成");
+ } catch (e) {
+ console.log(e);
+ ElMessage.error("下载失败");
+ } finally {
+ loading.close();
+ }
+}
+
+async function runWithLimit(tasks, limit = 3) {
+ const results = [];
+ const executing = [];
+
+ for (const task of tasks) {
+ const p = task().then((res) => {
+ executing.splice(executing.indexOf(p), 1);
+ return res;
+ });
+
+ results.push(p);
+ executing.push(p);
+
+ if (executing.length >= limit) {
+ await Promise.race(executing);
+ }
+ }
+
+ return Promise.all(results);
+}
+
+async function handleDownloadBatch() {
+ const $table = tableRef.value;
+ if (!$table) return;
+
+ const records = $table.getCheckboxRecords();
+
+ if (records.length === 0) {
+ ElMessage.warning("请选择需要下载的数据");
+ return;
+ }
+
+ const loading = ElLoading.service({
+ lock: true,
+ text: "正在下载并打包文件,请稍候...",
+ });
+
+ const zip = new JSZip();
+
+ try {
+ let total = 0;
+ let finished = 0;
+
+ // 👉 先统计总文件数(用于进度)
+ for (const item of records) {
+ const fileRes = await loadDataApi(
+ "bf_file",
+ `father = '${item.b_id}'`,
+ "",
+ );
+ if (fileRes.success) {
+ total += (fileRes.data || []).length;
+ }
+ }
+
+ // 👉 正式处理
+ for (const item of records) {
+ const folderName = `${item.b_container_no}_${formatToDate(
+ item.b_inputdatetime,
+ )}`;
+
+ const folder = zip.folder(folderName);
+
+ const fileRes = await loadDataApi(
+ "bf_file",
+ `father = '${item.b_id}'`,
+ "mx_xh asc",
+ );
+
+ if (!fileRes.success) {
+ ElMessage.error("获取附件失败");
+ continue;
+ }
+
+ const files = fileRes.data || [];
+
+ const tasks = files.map((file, index) => async () => {
+ const fileUrl = file.mx_mapfilename;
+
+ try {
+ const res = await fetch(fileUrl);
+ const blob = await res.blob();
+
+ const fileName =
+ buildFileName(item.b_container_no, index + 1, file.mx_filename) ||
+ "未命名文件";
+
+ folder.file(fileName, blob);
+ } catch (e) {
+ ElMessage.error(item.b_container_no + "箱号部分图片下载失败");
+ console.error("下载失败:", fileUrl);
+ } finally {
+ finished++;
+ loading.setText(`正在下载 ${finished}/${total} 个文件...`);
+ }
+ });
+
+ // 控制并发数
+ await runWithLimit(tasks, 3);
+ }
+
+ // 👉 生成 zip
+ loading.setText("正在生成压缩包...");
+ const content = await zip.generateAsync({ type: "blob" });
+
+ saveAs(content, "批量下载.zip");
+
+ ElMessage.success("下载完成");
+ } catch (error) {
+ console.error(error);
+ ElMessage.error("打包失败");
+ } finally {
+ loading.close();
+ }
+}
+
+function buildFileName(containerNo, index, fileName) {
+ const formattedIndex = String(index).padStart(2, "0");
+
+ return `${containerNo}_${formattedIndex}.${fileName.split(".").pop()}`;
}
@@ -299,7 +478,7 @@ function handleDownloadOne(data) {
-
+
@@ -368,7 +547,14 @@ function handleDownloadOne(data) {
}"
height="100%"
>
-
+
- {{ formatValueByFormat(row[column.col_FieldName], column.col_DisplayFormat) }}
+ {{
+ formatValueByFormat(
+ row[column.col_FieldName],
+ column.col_DisplayFormat,
+ )
+ }}
diff --git a/code/container-ocr/ocr-vue/src/views/index.vue b/code/container-ocr/ocr-vue/src/views/index.vue
index 40633f2e..3945bab6 100644
--- a/code/container-ocr/ocr-vue/src/views/index.vue
+++ b/code/container-ocr/ocr-vue/src/views/index.vue
@@ -1,49 +1,30 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ 测试