Files
workspace/code/container-ocr/ocr-vue/src/views/container/index.vue
T
2026-03-30 17:27:19 +08:00

783 lines
20 KiB
Vue

<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,
loadPageDataApi,
loadViewColumnsApi,
saveObjectApi,
} from "@/request/api";
import { formatValueByFormat } from "@/utils/columns";
import { updateViewTableColumns } from "@/utils/columns";
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 } from "@/utils/excel";
import { buildPublicUrl } from "@/utils/file";
const viewName = "v_container";
const tableRef = ref();
const permissionStore = usePermissionStore();
const columns = ref([]);
const tableData = ref([]);
const showSearch = ref(true);
const loading = reactive({
table: true,
});
const showHighDownload = ref(false);
const showUpdate = ref(false);
const searchForm = ref({
startTime: "",
endTime: "",
containerNo: "",
username: "",
});
const pagination = reactive({
page: 1,
size: 20,
total: 0,
});
const fileCategories = [
{
label: "集装箱号",
cateId: "0",
},
{
label: "其它照片",
cateId: "1",
},
];
const showPreview = ref(false);
const previewIndex = ref(-1);
const previewImageList = computed(() => {
return tableData.value.map((item) => item.b_container_img).filter((img) => img);
});
const currentRow = ref({});
const showFile = ref(false);
onMounted(async () => {
loading.table = true;
await loadTableColumns();
await loadTableDatas();
loading.table = false;
});
const handleSizeChange = (val) => {
pagination.size = val;
loadTableDatas();
};
const handleCurrentChange = (val) => {
pagination.page = val;
loadTableDatas();
};
async function loadTableColumns() {
const columnRes = await loadViewColumnsApi(viewName);
if (columnRes.success) {
columns.value = columnRes.data;
}
}
/**
* 上传到临时文件夹
*/
async function loadTableDatas() {
loading.table = true;
let searchCondition = "";
if (searchForm.value.startTime) {
searchCondition += `b_inputdatetime >= '${searchForm.value.startTime}' AND `;
}
if (searchForm.value.endTime) {
searchCondition += `b_inputdatetime <= '${searchForm.value.endTime}' AND `;
}
if (searchForm.value.containerNo) {
searchCondition += `b_container_no LIKE '%${searchForm.value.containerNo}%' AND `;
}
if (searchForm.value.username) {
searchCondition += `v_username LIKE '%${searchForm.value.username}%' AND `;
}
if (searchCondition.endsWith(" AND ")) {
searchCondition = searchCondition.slice(0, -4);
}
await loadPageDataApi({
viewName,
page: pagination.page,
size: pagination.size,
searchCondition,
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;
loading.table = false;
}
});
}
// 删除单行
async function handleDeleteRow(row) {
if (!row) return;
const $table = tableRef.value;
if ($table) {
deleteRows([row]);
}
}
// 删除多选行
async function handleDeleteRows() {
const $table = tableRef.value;
if ($table) {
deleteRows($table.getCheckboxRecords());
}
}
async function deleteRows(rows = []) {
const $table = tableRef.value;
if (!$table) {
return;
}
if (!rows.length) {
ElMessage.warning("请先选择要删除的行");
return;
}
const containerNos = rows.map((row) => row.b_container_no);
const deleteIds = rows.map((row) => row.b_id);
const tipMessage = `确定要删除以下箱号记录吗?<br/><strong>${containerNos.join(", ")}</strong>`;
await ElMessageBox.confirm(tipMessage, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
dangerouslyUseHTMLString: true,
});
if (deleteIds.length === 0) {
ElMessage.success("删除成功");
loadTableDatas();
return;
}
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;
}
await Promise.all(fileRes.data.map((file) => 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() {
pagination.page = 1;
loadTableDatas();
}
function handleSearchReset() {
searchForm.value.startTime = "";
searchForm.value.endTime = "";
searchForm.value.containerNo = "";
searchForm.value.username = "";
}
function handlePreviewShow(imgUrl) {
const index = previewImageList.value.indexOf(imgUrl);
if (index !== -1) {
previewIndex.value = index;
showPreview.value = true;
} else {
showPreview.value = false;
}
}
function handlePreviewClose() {
showPreview.value = false;
previewIndex.value = -1;
}
function handleFileShow(data) {
currentRow.value = data;
showFile.value = true;
}
function handleUpdateShow(data) {
currentRow.value = data;
showUpdate.value = true;
}
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: "正在下载并打包文件,请稍候...",
});
try {
const zip = new JSZip();
let finished = 0;
let totalFiles = 0;
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 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 getFileBlob(file.mx_mapfilename);
const fileName = buildFileName(containerNo, idx + 1, file.mx_filename) || "未命名文件";
folder.file(fileName, blob);
} catch (e) {
console.error(containerNo + "下载失败:", file.mx_mapfilename, e);
} finally {
finished++;
loading.setText(`正在下载 ${finished}/${totalFiles} 个文件...`);
}
});
});
}
// =========================
// 并发执行(限制3个)
// =========================
await runWithLimit(allTasks, 3);
loading.setText("正在生成压缩包...");
const content = await zip.generateAsync({ type: "blob" });
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");
return `${containerNo}_${formattedIndex}.${fileName.split(".").pop()}`;
}
</script>
<template>
<div class="g3-container">
<div v-if="showSearch" class="g3-search">
<G3Grid class="grid">
<G3GridGroup :label-width="80" :column="2">
<G3GridItem label="上传日期" :span="2">
<el-date-picker
type="datetime"
placeholder="开始日期"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
v-model="searchForm.startTime"
/>
<el-divider direction="vertical" />
<el-date-picker
type="datetime"
placeholder="结束日期"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
v-model="searchForm.endTime"
/>
</G3GridItem>
<G3GridItem label="箱号">
<el-input v-model="searchForm.containerNo" />
</G3GridItem>
<G3GridItem label="操作员">
<G3Select
v-model="searchForm.username"
v-model:label="searchForm.username"
search-condition="b_id <> 'g3soft'"
view-name="b_user"
value-key="b_name"
label-key="b_name"
keep-input
/>
</G3GridItem>
</G3GridGroup>
</G3Grid>
<div class="search-extra">
<el-button type="primary" @click="handleSearch">
<el-icon class="el-icon--left">
<G3Icon icon="lucide:search" />
</el-icon>
搜索
</el-button>
<el-button @click="handleSearchReset">
<el-icon class="el-icon--left">
<G3Icon icon="lucide:refresh-ccw" />
</el-icon>
重置
</el-button>
</div>
</div>
<div class="g3-content">
<div class="content-top">
<div>
<el-button
type="primary"
@click="handleDownloadBatch"
:disabled="!permissionStore.hasActionPermission(1)"
>
<el-icon class="el-icon--left">
<G3Icon icon="lucide:cloud-download" />
</el-icon>
批量下载
</el-button>
<el-button
type="primary"
:disabled="!permissionStore.hasActionPermission(1)"
@click="showHighDownload = true"
>
<el-icon class="el-icon--left">
<G3Icon icon="lucide:cloud-download" />
</el-icon>
高级下载
</el-button>
<el-button
type="danger"
@click="handleDeleteRows"
:disabled="!permissionStore.hasActionPermission(2)"
>
<el-icon class="el-icon--left">
<G3Icon icon="lucide:trash-2" />
</el-icon>
批量删除
</el-button>
</div>
<div>
<el-tooltip content="显示/关闭 搜索">
<el-button circle @click="showSearch = !showSearch">
<el-icon>
<G3Icon icon="lucide:search" />
</el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="刷新">
<el-button circle @click="loadTableDatas">
<el-icon>
<G3Icon icon="lucide:refresh-ccw" />
</el-icon>
</el-button>
</el-tooltip>
<el-dropdown>
<el-button circle style="margin-left: 12px">
<el-icon>
<G3Icon icon="lucide:ellipsis" />
</el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="updateViewTableColumns(tableRef)">
栏位保存
</el-dropdown-item>
<el-dropdown-item @click="ExportVxeTableExcel(tableRef)">
导出Execl
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<div class="g3-table">
<vxe-table
ref="tableRef"
:loading="loading.table"
:data="tableData"
:cell-config="{ height: 100 }"
:column-config="{
resizable: true,
drag: true,
}"
:row-config="{
useKey: true,
isCurrent: true,
isHover: true,
}"
:filter-config="{
showIcon: false,
}"
:column-drag-config="{
showIcon: false,
trigger: 'cell',
}"
height="100%"
show-overflow
show-header-overflow
>
<vxe-column field="seq" type="seq" width="50" align="center" title=" " fixed="left" />
<vxe-column type="checkbox" width="50" align="center" fixed="left" />
<vxe-column
v-for="column in columns"
:key="column.col_FieldName"
:field="column.col_FieldName"
:title="column.col_Caption"
:width="column.col_Width"
align="center"
:params="column"
:sortable="column.col_FieldName !== 'b_container_img'"
>
<template #default="{ row }">
<template v-if="column.col_FieldName === 'b_container_img'">
<el-image
style="width: 100px; height: 100px; cursor: pointer"
:src="row.b_container_img"
@click="handlePreviewShow(row.b_container_img)"
/>
</template>
<template v-else>
{{ formatValueByFormat(row[column.col_FieldName], column.col_DisplayFormat) }}
</template>
</template>
</vxe-column>
<vxe-column width="220" align="center" fixed="right" title="操作">
<template #default="{ row }">
<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)"
:disabled="!permissionStore.hasActionPermission(2)"
>
<el-icon class="el-icon--left">
<G3Icon icon="lucide:trash-2" />
</el-icon>
删除
</el-button> -->
</template>
</vxe-column>
</vxe-table>
</div>
<div class="g3-pagination">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.size"
:page-sizes="[20, 50, 100, 200]"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
<el-image-viewer
v-if="showPreview"
:url-list="previewImageList"
show-progress
:initial-index="previewIndex"
@close="handlePreviewClose"
/>
<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>
<style lang="scss" scoped>
.g3-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.g3-search {
display: flex;
align-items: center;
padding: 10px;
background-color: #fff;
margin-bottom: 12px;
.grid {
width: 500px;
}
.search-extra {
display: flex;
margin-left: 24px;
}
}
.g3-content {
flex-grow: 1;
background-color: #fff;
padding: 10px;
min-height: 0;
display: flex;
flex-direction: column;
.content-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
::v-deep(.el-button) {
height: 32px !important;
}
}
.g3-table {
flex: 1;
min-height: 0;
}
.g3-pagination {
margin-top: 8px;
display: flex;
align-items: center;
justify-content: flex-end;
flex-shrink: 0;
min-height: 0;
}
}
</style>