diff --git a/code/container-ocr/ocr-uniapp/env/.env b/code/container-ocr/ocr-uniapp/env/.env index 802f200a..67c258c8 100644 --- a/code/container-ocr/ocr-uniapp/env/.env +++ b/code/container-ocr/ocr-uniapp/env/.env @@ -1,3 +1,3 @@ -VITE_APP_PORT = 8080 +VITE_APP_PORT = 8081 VITE_ALIYUN_BUCKET = 'g3hy' \ No newline at end of file diff --git a/code/container-ocr/ocr-uniapp/src/static/db.json b/code/container-ocr/ocr-uniapp/src/static/db.json index 9901d445..a1722570 100644 --- a/code/container-ocr/ocr-uniapp/src/static/db.json +++ b/code/container-ocr/ocr-uniapp/src/static/db.json @@ -1,3 +1,3 @@ { - "g3hd": "http://192.168.1.137:8085/api" + "g3hd": "http://127.0.0.1:8085/api" } diff --git a/code/container-ocr/ocr-uniapp/test.html b/code/container-ocr/ocr-uniapp/test.html index 55234f20..6cd9c627 100644 --- a/code/container-ocr/ocr-uniapp/test.html +++ b/code/container-ocr/ocr-uniapp/test.html @@ -1,470 +1,187 @@ - - + - - - 拖拽触发文件下载演示 - + + - -
-

拖拽触发文件下载演示

-

演示如何通过拖拽操作触发浏览器文件下载

-
+ -
-
-

演示区域

-

将下面的元素拖拽到桌面或文件夹,会触发文件下载到浏览器的默认下载目录。

+
-
-
📦
-

拖拽我下载文件

-

按住鼠标左键拖拽到桌面或文件夹

-
+ +
+

数据看板

+ +
-
-

提示: 文件将下载到浏览器的默认下载目录(如"下载"文件夹),而不是直接保存到拖拽的目标位置。

-
+ +
-
-

可下载的文件:

-
-
📄
-
-
示例文档.txt
-
文本文件,约 20 字节
-
-
-
-
🖼️
-
-
demo-image.png
-
图片文件,约 45 KB
-
-
-
-
📊
-
-
数据报告.pdf
-
PDF文档,约 120 KB
-
-
-
+ +
+
近7天上传趋势
+
+
-
-
+ +
-
-

技术原理

-

这个演示通过以下步骤实现拖拽触发下载:

-
    -
  1. 为可拖拽元素添加 draggable="true" 属性
  2. -
  3. 监听 dragstart 事件
  4. -
  5. 在事件处理函数中,动态创建隐藏的下载链接
  6. -
  7. 设置链接的 href 和 download 属性
  8. -
  9. 通过 JavaScript 模拟点击链接,触发下载
  10. -
- -
-
// 获取可拖拽元素
-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); // 文件URL
-    link.download = getFileName(fileType); // 下载的文件名
-    
-    // 模拟点击触发下载
-    document.body.appendChild(link);
-    link.click();
-    document.body.removeChild(link);
-    
-    // 显示状态消息
-    showStatus('下载已开始,请检查浏览器的下载列表', 'success');
-});
-
-
+ +
+
操作员上传排行
+
-
-

技术限制与注意事项

-
    -
  • 拖拽操作实际上触发的是浏览器的标准下载流程,文件会保存到默认下载目录,而不是拖拽的目标位置
  • -
  • 某些浏览器可能对拖拽触发下载有安全限制
  • -
  • 下载行为受浏览器下载设置的影响(如下载前询问保存位置)
  • -
  • 需要服务器正确设置CORS头部(如果文件来自不同源)
  • -
  • 对于大文件,下载可能在后台进行,用户可能不会立即察觉
  • -
+ +
+ + +
+
最新上传
+ + + + + + + +
箱号操作员时间数量
+
+ + +
+
📢 企业公告
+
+
+
-
-

演示:拖拽触发文件下载 | 使用 HTML、CSS 和 JavaScript 实现

-

注意:这是一个前端技术演示,实际文件下载功能需要配合服务器端或Blob API

-
+
- - // 拖拽结束事件 - 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%" > - + 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 @@