700 lines
16 KiB
Vue
700 lines
16 KiB
Vue
<script lang="js" setup>
|
|
import {
|
|
deleteFileApi,
|
|
loadDataApi,
|
|
loadPageDataApi,
|
|
loadViewColumnsApi,
|
|
saveObjectApi,
|
|
} from "@/request/api";
|
|
import { getAlignByFormat, formatValueByFormat } from "@/utils/columns";
|
|
import { updateViewTableColumns } from "@/utils/columns";
|
|
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();
|
|
|
|
const columns = ref([]);
|
|
const tableData = ref([]);
|
|
|
|
const showSearch = ref(true);
|
|
const loading = reactive({
|
|
table: true,
|
|
});
|
|
|
|
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 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);
|
|
});
|
|
|
|
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) {
|
|
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;
|
|
}
|
|
|
|
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() {
|
|
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;
|
|
}
|
|
|
|
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()}`;
|
|
}
|
|
</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="操作员">
|
|
<el-input v-model="searchForm.username" />
|
|
</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">
|
|
<el-icon class="el-icon--left">
|
|
<G3Icon icon="lucide:cloud-download" />
|
|
</el-icon>
|
|
批量下载
|
|
</el-button>
|
|
<el-button type="danger" @click="handleDeleteRows">
|
|
<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>
|
|
<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>导出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%"
|
|
>
|
|
<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 }">
|
|
<el-button link type="primary" @click="handleDownloadOne(row)">
|
|
<el-icon class="el-icon--left">
|
|
<G3Icon icon="lucide:cloud-download" />
|
|
</el-icon>
|
|
下载
|
|
</el-button>
|
|
<el-button link type="primary" @click="handleFileShow(row)">
|
|
<el-icon class="el-icon--left">
|
|
<G3Icon icon="lucide:file-image" />
|
|
</el-icon>
|
|
图片
|
|
</el-button>
|
|
<el-button link type="danger" @click="handleDeleteRow(row)">
|
|
<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"
|
|
/>
|
|
|
|
<G3File
|
|
v-model="showFile"
|
|
:father="currentRow.b_id"
|
|
module-id="b_container"
|
|
:categories="fileCategories"
|
|
/>
|
|
</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>
|