This commit is contained in:
oneao committed 2026-04-22 17:28:50 +08:00
1 parent b7f2efc7c3
commit cc88205cba
23 files changed
+1239 -799

No files matched your search

@@ -41,7 +41,6 @@ public class FileController {
// 3. 物理路径
String basePath = fileConfig.getTempPath(); // D:/data/file/temp/
System.out.println(basePath);
// 4. 保存
FileUtils.saveFile(file, basePath, uuidName);
@@ -99,7 +98,7 @@ public class FileController {
// 8. 返回
Map<String, Object> result = new HashMap<>();
result.put("fileName", uuidName);
result.put("fileName", fileName);
result.put("url", url);
result.put("path", relativePath);
@@ -237,7 +236,7 @@ public class FileController {
result.put("url", url);
result.put("path", relativePath);
result.put("uid", nextId);
result.put("fileName", newFileName);
result.put("fileName", fileName);
result.put("bucket", bucket);
return Result.ok(result);
@@ -1,10 +1,10 @@
url=jdbc:sqlserver://118.89.70.199:1433;databaseName=G3BoxOCR
username=sa
password=zhang520..
driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
#url=jdbc:sqlserver://127.0.0.1:1433;databaseName=G3ContainerOCR
#url=jdbc:sqlserver://118.89.70.199:1433;databaseName=G3BoxOCR
#username=sa
#password=123456@aaa
#password=zhang520..
#driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
url=jdbc:sqlserver://127.0.0.1:1433;databaseName=G3ContainerOCR
username=sa
password=123456@aaa
driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
@@ -14,7 +14,8 @@ const form = ref<any>({
b_container_img: '',
})
const subFileList = ref([])
const subFileList = ref([]) // 装货照片
const subFileList2 = ref([]) // 发货单、签收单、过磅单
onLoad((options) => {
const id = options.id
@@ -34,7 +35,7 @@ async function loadMainData() {
try {
const orgUrl = await getOrgBaseUrl(userStore.userInfo.orgid)
// 并发请求(提升性能)
// ================= 并发请求 =================
const [res, fileRes] = await Promise.all([
loadDataApi({
viewName: 'v_container',
@@ -42,7 +43,7 @@ async function loadMainData() {
}),
loadDataApi({
viewName: 'bf_file',
searchCondition: `father = '${form.value.b_id}' AND mx_cate_id = '1'`,
searchCondition: `father = '${form.value.b_id}'`,
}),
])
@@ -60,19 +61,46 @@ async function loadMainData() {
form.value = mainData
}
else {
console.warn('未获取到主数据')
uni.showToast({
title: '当前数据无效',
icon: 'none',
duration: 1500,
})
setTimeout(() => {
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
}
else {
uni.reLaunch({
url: '/',
})
}
}, 1500)
return
}
// ================= 子表 =================
// ================= 子表统一处理 =================
if (fileRes.success) {
const files = (fileRes.data || []).map(file => ({
const allFiles = fileRes.data || []
const formatFile = file => ({
...file,
mx_mapfilename: buildPubicUrl(orgUrl, file.mx_mapfilename),
name: file.mx_filename,
url: buildPubicUrl(orgUrl, file.mx_mapfilename),
}))
})
subFileList.value = files
// 按分类拆分
subFileList.value = allFiles
.filter(f => f.mx_cate_id === '1')
.map(formatFile)
subFileList2.value = allFiles
.filter(f => f.mx_cate_id === '2')
.map(formatFile)
}
}
catch (err) {
@@ -150,48 +178,68 @@ async function handleConfirm() {
})
const fileInserts = []
const fileUpdates = []
// 子表
for (let index = 0; index < subFileList.value.length; index++) {
const fileItem = subFileList.value[index]
const allFiles = [
...subFileList.value.map(item => ({ ...item, cate: '1' })),
...subFileList2.value.map(item => ({ ...item, cate: '2' })),
]
if (fileItem.subid) {
// 已上传的文件,无需重复上传
continue
}
const grouped = allFiles.reduce((acc, item) => {
if (!acc[item.cate])
acc[item.cate] = []
acc[item.cate].push(item)
return acc
}, {})
const res = await uploadContainerFileApi(fileItem.file.path, {
jobId: form.value.b_id,
containerNo: form.value.b_container_no,
fileName: fileItem.name,
})
for (const cate in grouped) {
let index = 1
const maxIndexNumber = Math.max(
0,
...subFileList.value.map(item => item.mx_xh || 0),
)
for (const fileItem of grouped[cate]) {
const mx_xh = index++
if (res.success) {
const indexNumber = maxIndexNumber + 1
// 已存在 → update
if (fileItem.subid) {
fileUpdates.push({
subid: fileItem.subid,
mx_xh,
mx_cate_id: cate,
})
continue
}
// 新增 → upload
const res = await uploadContainerFileApi(fileItem.file.path, {
jobId: form.value.b_id,
containerNo: form.value.b_container_no,
fileName: fileItem.name,
})
if (!res.success)
continue
fileInserts.push({
subid: res.data.uid,
father: form.value.b_id,
mx_xh: indexNumber,
mx_xh,
mx_filename: res.data.fileName,
mx_filesize: fileItem.file.size,
mx_fileext: getFileExtension(fileItem.file.name),
mx_mapfilename: res.data.path,
mx_moduleid: 'b_container',
mx_cate_id: '1',
mx_cate_id: cate,
})
fileItem.subid = res.data.uid
}
}
if (fileInserts.length === 0) {
if (fileInserts.length === 0 && fileUpdates.length === 0) {
toast.success('保存成功')
setTimeout(() => {
loadMainData()
}, 1000)
return
}
@@ -200,7 +248,7 @@ async function handleConfirm() {
tableName: 'bf_file',
keyField: 'subid',
inserts: fileInserts,
updates: [],
updates: fileUpdates,
deletes: [],
},
]
@@ -208,6 +256,10 @@ async function handleConfirm() {
saveObjectApi(reqData).then((res) => {
if (res.success) {
toast.success('保存成功')
setTimeout(() => {
loadMainData()
}, 1000)
}
})
}
@@ -225,6 +277,7 @@ function validateContainerNo(containerNo) {
return false
}
}
const templateCollapse = ref<string[]>(['template1'])
</script>
<template>
@@ -272,12 +325,62 @@ function validateContainerNo(containerNo) {
</view>
</wd-cell-group>
<wd-collapse v-model="templateCollapse" style="margin-top: 32rpx;">
<wd-collapse-item name="template1">
<template #title>
<view class="template-header">
<view class="title">
<sar-icon
name="file"
size="38rpx"
style="margin-right: 16rpx;"
/>
<text>
上传模板(参考)
</text>
</view>
<sar-icon
:name="templateCollapse.includes('template1') ? 'up' : 'down'"
size="38rpx"
/>
</view>
</template>
<view class="template-container">
<view class="item">
<text class="title">
1、提空箱拍照模版
</text>
<wd-img
style="width:100%;height:400rpx;"
src="/static/images/D21304CD0CDAB09EE3349E8B7D078CC9.jpg"
:enable-preview="true"
@click.stop
/>
</view>
<view class="item" style="margin-left:16rpx">
<text class="title">
2、装箱拍照模版
</text>
<wd-img
style="width:100%;height:400rpx;"
src="/static/images/027C52B0A3986905F161127336244D72.jpg"
:enable-preview="true"
@click.stop
/>
</view>
</view>
</wd-collapse-item>
</wd-collapse>
<wd-cell-group custom-class="group" border use-slot style="margin-top: 32rpx;">
<template #title>
<view style="display:flex;align-items: center;">
<sar-icon family="iconfont" name="tupian" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
<text>
其他图片
装货照片
</text>
</view>
</template>
@@ -292,6 +395,27 @@ function validateContainerNo(containerNo) {
/>
</view>
</wd-cell-group>
<wd-cell-group custom-class="group" border use-slot style="margin-top: 32rpx;">
<template #title>
<view style="display:flex;align-items: center;">
<sar-icon family="iconfont" name="tupian" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
<text>
发货单、签收单、过磅单
</text>
</view>
</template>
<view class="step-content">
<sar-upload
v-model="subFileList2"
multiple
:before-read="beforeRead"
:before-remove="beforeRemove"
@item-click="onItemClick"
@remove="onRemove"
/>
</view>
</wd-cell-group>
</view>
<view class="g3-footer">
<view class="footer-main">
@@ -465,4 +589,34 @@ page {
transform: scale(0.98);
box-shadow: 0 3rpx 6rpx rgba(58, 110, 232, 0.15);
}
.template-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
font-weight: 500;
.title {
display: flex;
align-items: center;
}
}
.template-container {
display: flex;
.item {
display: flex;
flex-direction: column;
width: 50%;
.title {
font-size: 28rpx;
font-weight: 600;
color: #333;
padding-left: 10rpx;
border-left: 6rpx solid var(--sar-primary);
}
}
}
</style>
@@ -19,7 +19,8 @@ const form = ref({
ocrNo: '',
})
const subFileList = ref([])
const subFileList = ref([]) // 装货照片
const subFileList2 = ref([]) // 发货单、签收单、过磅单
const tipContainerNo = ref('')
@@ -167,7 +168,7 @@ async function handleConfirm() {
b_ocr_score: form.value.ocrScore || 0.00,
}
const fileInserts = []
// 主表
await uploadContainerFileApi(boxImage.value.path, {
jobId: form.value.b_id,
@@ -191,31 +192,47 @@ async function handleConfirm() {
}
})
// 子表
for (let index = 0; index < subFileList.value.length; index++) {
const fileItem = subFileList.value[index]
const indexMap = {}
const getNextIndex = (cate) => {
if (!indexMap[cate]) {
indexMap[cate] = 1
}
return indexMap[cate]++
}
const allFiles = [
...subFileList.value.map(item => ({
...item,
cate: '1',
})),
...subFileList2.value.map(item => ({
...item,
cate: '2',
})),
]
for (const fileItem of allFiles) {
const res = await uploadContainerFileApi(fileItem.file.path, {
jobId: form.value.b_id,
containerNo: form.value.b_container_no,
fileName: fileItem.name,
})
if (res.success) {
const indexNumber = index + 2
if (!res.success)
continue
fileInserts.push({
subid: res.data.uid,
father: form.value.b_id,
mx_xh: indexNumber,
mx_filename: res.data.fileName,
mx_filesize: fileItem.file.size,
mx_fileext: getFileExtension(fileItem.file.name),
mx_mapfilename: res.data.path,
mx_moduleid: 'b_container',
mx_cate_id: '1',
})
}
fileInserts.push({
subid: res.data.uid,
father: form.value.b_id,
mx_xh: getNextIndex(fileItem.cate),
mx_filename: res.data.fileName,
mx_filesize: fileItem.file.size,
mx_fileext: getFileExtension(fileItem.file.name),
mx_mapfilename: res.data.path,
mx_moduleid: 'b_container',
mx_cate_id: fileItem.cate,
})
}
const reqData = [
@@ -271,11 +288,15 @@ async function continueAdd() {
form.value.b_id = idRes.data
subFileList.value = []
subFileList2.value = []
showResult.value = false
tipContainerNo.value = ''
}
}
const templateCollapse = ref<string[]>(['template1'])
</script>
<template>
@@ -359,12 +380,62 @@ async function continueAdd() {
</view>
</wd-cell-group>
<wd-collapse v-model="templateCollapse" style="margin-top: 32rpx;">
<wd-collapse-item name="template1">
<template #title>
<view class="template-header">
<view class="title">
<sar-icon
name="file"
size="38rpx"
style="margin-right: 16rpx;"
/>
<text>
上传模板(参考)
</text>
</view>
<sar-icon
:name="templateCollapse.includes('template1') ? 'up' : 'down'"
size="38rpx"
/>
</view>
</template>
<view class="template-container">
<view class="item">
<text class="title">
1、提空箱拍照模版
</text>
<wd-img
style="width:100%;height:400rpx;"
src="/static/images/D21304CD0CDAB09EE3349E8B7D078CC9.jpg"
:enable-preview="true"
@click.stop
/>
</view>
<view class="item" style="margin-left:16rpx">
<text class="title">
2、装箱拍照模版
</text>
<wd-img
style="width:100%;height:400rpx;"
src="/static/images/027C52B0A3986905F161127336244D72.jpg"
:enable-preview="true"
@click.stop
/>
</view>
</view>
</wd-collapse-item>
</wd-collapse>
<wd-cell-group custom-class="group" border use-slot style="margin-top: 32rpx;">
<template #title>
<view style="display:flex;align-items: center;">
<sar-icon family="iconfont" name="tupian" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
<text>
其他图片
装货照片
</text>
</view>
</template>
@@ -378,6 +449,25 @@ async function continueAdd() {
/>
</view>
</wd-cell-group>
<wd-cell-group custom-class="group" border use-slot style="margin-top: 32rpx;">
<template #title>
<view style="display:flex;align-items: center;">
<sar-icon family="iconfont" name="tupian" size="38rpx" color="var(--sar-primary)" style="margin-right: 16rpx;" />
<text>
发货单、签收单、过磅单
</text>
</view>
</template>
<view class="step-content">
<sar-upload
v-model="subFileList2"
multiple
:before-read="beforeRead"
:before-remove="beforeRemove"
@item-click="onItemClick"
/>
</view>
</wd-cell-group>
</view>
<view class="g3-footer">
<view class="footer-main">
@@ -568,4 +658,34 @@ page {
transform: scale(0.98);
box-shadow: 0 3rpx 6rpx rgba(58, 110, 232, 0.15);
}
.template-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
font-weight: 500;
.title {
display: flex;
align-items: center;
}
}
.template-container {
display: flex;
.item {
display: flex;
flex-direction: column;
width: 50%;
.title {
font-size: 28rpx;
font-weight: 600;
color: #333;
padding-left: 10rpx;
border-left: 6rpx solid var(--sar-primary);
}
}
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

+2 -2
View File
@@ -10,7 +10,7 @@ PUBLIC_SERVER_PORT=8080
# API
# 后端 API 基础地址
# PUBLIC_API_BASE_URL=http://ser369.g3soft.cn:80/api
PUBLIC_API_BASE_URL=http://192.168.1.137:80/api
PUBLIC_API_BASE_URL=http://ser369.g3soft.cn:80/api
# PUBLIC_API_BASE_URL=http://192.168.1.137:80/api
PUBLIC_ALIYUN_BUCKET = g3hd
+8
View File
@@ -18,6 +18,7 @@ declare module 'vue' {
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
@@ -27,16 +28,23 @@ declare module 'vue' {
ElInput: typeof import('element-plus/es')['ElInput']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElResult: typeof import('element-plus/es')['ElResult']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
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']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElText: typeof import('element-plus/es')['ElText']
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']
@@ -130,6 +130,9 @@ function handleUpdateDialogClosed() {
function handleShowUpdateDialog(file) {
updateFile.value = JSON.parse(JSON.stringify(file));
updateFile.value._main = file.mx_cate_id === "0";
showUpdateDialog.value = true;
}
@@ -162,7 +165,6 @@ async function handleConfirmUpdate() {
subid: updateFile.value.subid,
mx_filename: updateFile.value.mx_filename,
mx_bz: updateFile.value.mx_bz,
mx_cate_id: updateFile.value.mx_cate_id,
},
],
deletes: [],
@@ -170,7 +172,7 @@ async function handleConfirmUpdate() {
];
// 检查是否设置了箱门照
if (updateFile.value.mx_cate_id === "0") {
if (updateFile.value._main) {
requestData[0].updates[0].mx_xh = "0";
// 检查数据库表其它是否有文件已经设置了箱门照
@@ -178,6 +180,7 @@ async function handleConfirmUpdate() {
const res = await loadDataApi("bf_file", searchCondition, "mx_xh", ["subid", "mx_xh"]);
if (res.success && res.data.length > 0) {
// 更新旧数据
requestData.push({
tableName: "bf_file",
keyField: "subid",
@@ -185,13 +188,32 @@ async function handleConfirmUpdate() {
updates: [
{
subid: res.data[0].subid,
mx_cate_id: "1",
mx_cate_id: updateFile.value.mx_cate_id,
mx_xh: updateFile.value.mx_xh,
},
],
deletes: [],
});
// 更新新数据
requestData[0].updates[0].mx_cate_id = "0";
// 更新主图
requestData.push({
tableName: "b_container",
keyField: "b_id",
inserts: [],
updates: [
{
b_id: props.father,
b_container_img: extractFilePath(updateFile.value.mx_mapfilename),
},
],
deletes: [],
});
} else {
requestData[0].updates[0].mx_cate_id = "0";
requestData.push({
tableName: "b_container",
keyField: "b_id",
@@ -215,7 +237,7 @@ async function handleConfirmUpdate() {
showUpdateDialog.value = false;
loadFiles()
loadFiles();
}
} catch (error) {
ElMessage.error("更新文件时发生错误");
@@ -287,7 +309,7 @@ async function loadFiles() {
}
try {
const res = await loadDataApi("bf_file", searchCondition, "mx_cate_id,mx_xh");
const res = await loadDataApi("bf_file", searchCondition, "mx_cate_id ASC, mx_xh ASC");
if (res.success) {
let datas = res.data.map((item) => {
@@ -385,69 +407,75 @@ function handleDrop(e) {
// 本地上传方法
async function uploadLocalFiles(files) {
if (files.length > 0) {
const currentLength = fileList.value.length;
if (!files.length) return;
Array.from(files).forEach((file, index) => {
const fileInfo = {
mx_filename: file.name,
mx_filesize: file.size,
mx_fileext: getFileExtension(file.name),
mx_inputuser_id: userStore.userInfo.userid,
mx_inputdatetime: "",
mx_mapfilename: "",
mx_xh: currentLength + index + 1,
_content: null,
_status: "pending",
};
const currentLength = fileList.value.length;
const reader = new FileReader();
Array.from(files).forEach((file, index) => {
const fileInfo = {
mx_filename: file.name,
mx_filesize: file.size,
mx_fileext: getFileExtension(file.name),
mx_inputuser_id: userStore.userInfo.userid,
mx_inputdatetime: "",
mx_mapfilename: "",
mx_xh: currentLength + index + 1,
reader.onload = async (event) => {
const blob = new Blob([event.target.result], { type: file.type });
_content: file, // 默认原始文件
_status: "pending",
};
if (file.type.startsWith("image/")) {
try {
const tempUrl = URL.createObjectURL(blob);
// ===== 图片处理 =====
if (file.type.startsWith("image/")) {
const tempUrl = URL.createObjectURL(file);
if (file.size > 512000) {
const { url, size, blob: compressedBlob } = await compressImage(tempUrl);
if (file.size > 512000) {
compressImage(tempUrl)
.then(({ url, size, blob }) => {
fileInfo.mx_mapfilename = url;
fileInfo.mx_filesize = size;
fileInfo.mx_mapfilename = url;
fileInfo.mx_filesize = size;
fileInfo._content = compressedBlob;
} else {
fileInfo.mx_mapfilename = tempUrl;
fileInfo._content = blob;
}
} catch (err) {
// 用压缩后的 File(推荐稳定结构)
fileInfo._content = new File([blob], file.name, {
type: file.type,
});
fileList.value.push(fileInfo);
})
.catch((err) => {
console.error("图片压缩失败:", err);
fileInfo.mx_mapfilename = URL.createObjectURL(blob);
}
} else {
fileInfo._content = blob;
}
fileInfo.mx_mapfilename = tempUrl;
fileInfo._content = file;
fileList.value.push(fileInfo);
});
} else {
fileInfo.mx_mapfilename = tempUrl;
fileInfo._content = file;
fileList.value.push(fileInfo);
};
}
reader.readAsArrayBuffer(file);
});
return;
}
nextTick(() => {
setTimeout(() => {
if (displayMode.value === 0) {
if (containerRef.value) {
containerRef.value.scrollTop = containerRef.value.scrollHeight;
}
} else {
if (tableRef.value) {
tableRef.value.scrollToEndRow();
}
}
}, 50);
});
}
// ===== 非图片:直接使用原文件 =====
fileInfo._content = file;
fileList.value.push(fileInfo);
});
nextTick(() => {
setTimeout(() => {
if (displayMode.value === 0) {
containerRef.value?.scrollTo?.({
top: containerRef.value.scrollHeight,
});
} else {
tableRef.value?.scrollToEndRow?.();
}
}, 50);
});
}
function isImage(fileExtension) {
@@ -646,7 +674,13 @@ async function deleteFiles(files) {
});
}
// 保存
// 保存
async function handleSave() {
if (cateId.value !== "1" && cateId.value !== "2") {
ElMessage.warning("只允许 装货、发货单 上传");
return;
}
loading.value = true;
loadingTip.value = "正在保存中 . . .";
@@ -658,19 +692,19 @@ async function handleSave() {
["_content", "_status", "checked"],
);
// 没有变化直接结束
if (!hasChanges) {
ElMessage.info("没有数据变更");
return;
}
const mxXh =
fileList.value.length > 0 ? Math.max(...fileList.value.map((item) => item.mx_xh ?? 0)) : 2;
// ===============================
// 1️⃣ 先处理新增(上传文件)
// ===============================
const fileInserts = [];
// 处理新增
for (const item of insertdata) {
try {
const uploadRes = await uploadContainerFileApi(item.mx_mapfilename, {
const uploadRes = await uploadContainerFileApi(item._content, {
jobId: props.father,
fileName: item.mx_filename,
containerNo: props.containerNo,
@@ -680,31 +714,85 @@ async function handleSave() {
throw new Error("上传失败");
}
const insertRes = await insertFileApi({
fileInserts.push({
subid: uploadRes.data.uid,
father: props.father,
mx_xh: mxXh + 1,
mx_filename: item.mx_filename,
mx_filesize: item.mx_filesize,
mx_fileext: item.mx_fileext,
mx_mapfilename: uploadRes.data.path,
mx_moduleid: "b_container",
mx_cate_id: "1",
mx_cate_id: cateId.value || "1",
});
if (!insertRes.success) {
throw new Error("入库失败");
}
console.log(`文件 ${item.mx_filename} 上传成功`);
item._status = "";
} catch (error) {
console.error(`文件 ${item.mx_filename} 处理失败:`, error);
ElMessage.error(`文件 ${item.mx_filename} 上传失败,请重试`);
}
}
// ===============================
// 2️⃣ 合并所有数据(旧 + 新 + 更新)
// ===============================
const mergedList = [
...fileList.value
.filter((item) => !insertdata.includes(item)) // 去掉旧 insert 原始对象
.map((item) => ({
...item,
type: "update",
})),
...fileInserts.map((item) => ({
...item,
type: "insert",
})),
];
// ===============================
// 3️⃣ 重新排序 mx_xh(核心)
// ===============================
const ordered = mergedList.map((item, index) => ({
...item,
mx_xh: index + 1,
}));
// ===============================
// 4️⃣ 拆分成 API 结构
// ===============================
const fileUpdates = ordered
.filter((item) => item.type === "update")
.map((item) => ({
subid: item.subid,
mx_xh: item.mx_xh,
}));
const finalInserts = ordered
.filter((item) => item.type === "insert")
.map(({ type, ...rest }) => rest);
const fileDeletes = deletedata.map((item) => ({
subid: item.subid,
}));
// ===============================
// 5️⃣ 统一提交
// ===============================
const reqData = [
{
tableName: "bf_file",
keyField: "subid",
inserts: finalInserts,
updates: fileUpdates,
deletes: fileDeletes,
},
];
await saveObjectApi(reqData);
ElMessage.success("保存成功");
} catch (error) {
console.error(error);
ElMessage.error("保存失败");
} finally {
loading.value = false;
loadFiles();
@@ -745,7 +833,6 @@ function onClosed() {
}
function handleTreeNodeClick(data) {
console.log(data);
cateId.value = data.cateId || "";
loadFiles();
@@ -760,7 +847,7 @@ function handleTreeNodeClick(data) {
<template>
<el-dialog
title="附件管理"
width="900"
width="1100"
draggable
modal-class="g3-file-modal"
v-bind="$attrs"
@@ -783,272 +870,288 @@ function handleTreeNodeClick(data) {
/>
<div class="g3-container">
<div v-if="categories[0].children.length > 0" class="g3-category">
<el-tree
ref="treeRef"
node-key="cateId"
:data="categories"
:default-expanded-keys="['']"
@node-click="handleTreeNodeClick"
/>
</div>
<div class="g3-file-main">
<div class="g3-header">
<div>
<el-checkbox
v-model="selectAll"
:indeterminate="indeterminate"
:disabled="loading"
@change="toggleSelectAll"
>
全选
</el-checkbox>
<el-button
size="small"
type="primary"
:disabled="checkedFiles.length === 0 || loading"
style="margin-left: 8px"
@click="handleDownloadZip"
>
<el-icon class="el-icon--left">
<G3Icon icon="lucide:download" />
</el-icon>
打包下载
</el-button>
<el-button
v-if="!props.readonly"
size="small"
type="danger"
:disabled="checkedFiles.length === 0 || loading"
@click="handleBatchDelete()"
>
<el-icon class="el-icon--left">
<G3Icon icon="lucide:trash-2" />
</el-icon>
批量删除
</el-button>
<el-splitter>
<el-splitter-panel size="20%" v-if="categories[0].children.length > 0">
<div class="g3-category">
<el-tree
ref="treeRef"
node-key="cateId"
:data="categories"
:default-expanded-keys="['']"
@node-click="handleTreeNodeClick"
/>
</div>
</el-splitter-panel>
<el-splitter-panel :min="400">
<div class="g3-file-main">
<div class="g3-header">
<div>
<el-checkbox
v-model="selectAll"
:indeterminate="indeterminate"
:disabled="loading"
@change="toggleSelectAll"
>
全选
</el-checkbox>
<div>
<el-dropdown>
<span class="g3-mode">
模式:{{ displayMode === 0 ? "卡片视图" : "列表视图" }}
<el-icon class="el-icon--right">
<G3Icon icon="lucide:chevron-down" />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleChangeDisplayMode(0)">
卡片视图
</el-dropdown-item>
<el-dropdown-item @click="handleChangeDisplayMode(1)">
列表视图
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<div
ref="containerRef"
v-loading="loading"
:element-loading-text="loadingTip"
class="g3-main"
tabindex="0"
@paste="handlePaste"
@dragover="handleDragOver"
@drop="handleDrop"
>
<div v-if="fileList.length === 0" class="g3-tip">
<el-icon size="64">
<G3Icon icon="lucide:cloud-upload" />
</el-icon>
<div>支持 拖拽文件、粘贴文件、多文件上传</div>
</div>
<div v-else-if="displayMode === 0" class="g3-files">
<div v-for="(file, index) in fileList" :key="index" class="g3-file-item">
<div class="g3-file-checkbox">
<el-checkbox v-model="file._checked" size="large" />
</div>
<!-- pending:未上传,completed:已上传,failed:上传失败 -->
<div v-if="file.mx_cate_id === '0'" class="g3-file-status status-primary">
<span>箱门照</span>
</div>
<div
v-if="file._status === 'pending' || file._status === 'failed'"
:class="`g3-file-status status-${file._status}`"
>
<span v-if="file._status === 'pending'">未上传</span>
<span v-if="file._status === 'failed'">上传失败</span>
</div>
<div class="g3-file-preview" @click.stop="handleShowPreview(file)">
<template v-if="getFileIcon(file.mx_fileext) === 'image'">
<el-image class="g3-file-preview-img" :src="file.mx_mapfilename" />
</template>
<template v-else>
<G3Icon :icon="getFileIcon(file.mx_fileext)" size="88" />
</template>
</div>
<div class="g3-file-info">
<el-tooltip :content="file.mx_filename">
<div class="g3-file-name">
{{ file.mx_filename }}
</div>
</el-tooltip>
<div class="g3-file-details">
<div>{{ formatFileSize(file.mx_filesize) }}</div>
<div>
{{ file.mx_updateuser_id || file.mx_inputuser_id }}
<template v-if="file.mx_inputdatetime || file.mx_updatedatetime">
|
{{ formatTime(file.mx_updatedatetime || file.mx_inputdatetime) }}
</template>
</div>
</div>
<div class="g3-file-actions">
<el-button
v-if="!props.readonly"
type="danger"
size="small"
circle
@click.stop="handleDeleteOne(file)"
>
<G3Icon icon="lucide:trash-2" />
</el-button>
<el-button
type="primary"
size="small"
circle
@click.stop="handleDownloadFile(file)"
>
<el-button
size="small"
type="primary"
:disabled="checkedFiles.length === 0 || loading"
style="margin-left: 8px"
@click="handleDownloadZip"
>
<el-icon class="el-icon--left">
<G3Icon icon="lucide:download" />
</el-button>
<el-dropdown v-if="!props.readonly" placement="top" trigger="click">
<el-button type="info" size="small" circle style="margin-left: 12px">
<G3Icon icon="lucide:ellipsis" />
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleShowUpdateDialog(file)">
修改文件信息
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-icon>
打包下载
</el-button>
<el-button
v-if="!props.readonly"
size="small"
type="danger"
:disabled="checkedFiles.length === 0 || loading"
@click="handleBatchDelete()"
>
<el-icon class="el-icon--left">
<G3Icon icon="lucide:trash-2" />
</el-icon>
批量删除
</el-button>
</div>
<div>
<el-dropdown>
<span class="g3-mode">
模式:{{ displayMode === 0 ? "卡片视图" : "列表视图" }}
<el-icon class="el-icon--right">
<G3Icon icon="lucide:chevron-down" />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleChangeDisplayMode(0)">
卡片视图
</el-dropdown-item>
<el-dropdown-item @click="handleChangeDisplayMode(1)">
列表视图
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<div v-else class="g3-table">
<div>
<vxe-table
ref="tableRef"
border
stripe
header-align="center"
:data="fileList"
height="100%"
show-overflow
show-header-overflow
:sort-config="{
trigger: 'cell',
}"
:filter-config="{
showIcon: false,
}"
size="small"
>
<vxe-column type="seq" width="50" title=" " align="center" />
<vxe-column width="50" align="center">
<template #default="{ row }">
<el-checkbox v-model="row._checked" size="large" />
</template>
</vxe-column>
<vxe-column field="mx_filename" title="文件名" sortable align="center">
<template #default="{ row }">
<el-button type="primary" link @click.stop="handleShowPreview(row)">
{{ row.mx_filename }}
</el-button>
</template>
</vxe-column>
<vxe-column field="mx_inputdatetime" title="上传日期" sortable align="center">
<template #default="{ row }">
{{ formatTime(row.mx_inputdatetime) }}
</template>
</vxe-column>
<vxe-column
field="mx_fileext"
title="文件类型"
sortable
width="100"
align="center"
/>
<vxe-column
field="mx_filesize"
title="文件大小"
sortable
width="100"
align="center"
>
<template #default="{ row }">
{{ formatFileSize(row.mx_filesize) }}
</template>
</vxe-column>
<vxe-column width="180" title="操作">
<template #default="{ row }">
<div
style="
display: flex;
align-items: center;
justify-content: center;
height: 28px;
"
>
<div
ref="containerRef"
v-loading="loading"
:element-loading-text="loadingTip"
class="g3-main"
tabindex="0"
@paste="handlePaste"
@dragover="handleDragOver"
@drop="handleDrop"
>
<div v-if="fileList.length === 0" class="g3-tip">
<el-icon size="64">
<G3Icon icon="lucide:cloud-upload" />
</el-icon>
<div>支持 拖拽文件、粘贴文件、多文件上传</div>
</div>
<div v-else-if="displayMode === 0" class="g3-files">
<div v-for="(file, index) in fileList" :key="index" class="g3-file-item">
<div class="g3-file-checkbox">
<el-checkbox v-model="file._checked" size="large" />
</div>
<!-- pending:未上传,completed:已上传,failed:上传失败 -->
<div v-if="file.mx_cate_id === '0'" class="g3-file-status status-primary">
<span>箱门照</span>
</div>
<div
v-if="file._status === 'pending' || file._status === 'failed'"
:class="`g3-file-status status-${file._status}`"
>
<span v-if="file._status === 'pending'">未上传</span>
<span v-if="file._status === 'failed'">上传失败</span>
</div>
<div class="g3-file-preview" @click.stop="handleShowPreview(file)">
<template v-if="getFileIcon(file.mx_fileext) === 'image'">
<el-image class="g3-file-preview-img" :src="file.mx_mapfilename" />
</template>
<template v-else>
<G3Icon :icon="getFileIcon(file.mx_fileext)" size="88" />
</template>
</div>
<div class="g3-file-info">
<el-tooltip :content="file.mx_filename">
<div class="g3-file-name">
{{ file.mx_filename }}
</div>
</el-tooltip>
<div class="g3-file-details">
<div>{{ formatFileSize(file.mx_filesize) }}</div>
<div>
{{ file.mx_updateuser_id || file.mx_inputuser_id }}
<template v-if="file.mx_inputdatetime || file.mx_updatedatetime">
|
{{ formatTime(file.mx_updatedatetime || file.mx_inputdatetime) }}
</template>
</div>
</div>
<div class="g3-file-actions">
<el-button
v-if="!props.readonly"
type="danger"
link
@click.stop="handleDeleteOne(row)"
size="small"
circle
@click.stop="handleDeleteOne(file)"
>
删除
<G3Icon icon="lucide:trash-2" />
</el-button>
<el-button type="primary" link @click.stop="handleDownloadFile(row)">
下载
<el-button
type="primary"
size="small"
circle
@click.stop="handleDownloadFile(file)"
>
<G3Icon icon="lucide:download" />
</el-button>
<el-dropdown v-if="!props.readonly" placement="top" trigger="click">
<el-button type="info" link style="margin-left: 12px"> 更多 </el-button>
<el-button type="info" size="small" circle style="margin-left: 12px">
<G3Icon icon="lucide:ellipsis" />
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleShowUpdateDialog(row)">
<el-dropdown-item @click="handleShowUpdateDialog(file)">
修改文件信息
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</vxe-column>
</vxe-table>
</div>
</div>
</div>
<div v-else class="g3-table">
<div>
<vxe-table
ref="tableRef"
border
stripe
header-align="center"
:data="fileList"
height="100%"
show-overflow
show-header-overflow
:sort-config="{
trigger: 'cell',
}"
:filter-config="{
showIcon: false,
}"
size="small"
>
<vxe-column type="seq" width="50" title=" " align="center" />
<vxe-column width="50" align="center">
<template #default="{ row }">
<el-checkbox v-model="row._checked" size="large" />
</template>
</vxe-column>
<vxe-column field="mx_filename" title="文件名" sortable align="center">
<template #default="{ row }">
<el-button type="primary" link @click.stop="handleShowPreview(row)">
{{ row.mx_filename }}
</el-button>
</template>
</vxe-column>
<vxe-column field="mx_inputdatetime" title="上传日期" sortable align="center">
<template #default="{ row }">
{{ formatTime(row.mx_inputdatetime) }}
</template>
</vxe-column>
<vxe-column
field="mx_fileext"
title="文件类型"
sortable
width="100"
align="center"
/>
<vxe-column
field="mx_filesize"
title="文件大小"
sortable
width="100"
align="center"
>
<template #default="{ row }">
{{ formatFileSize(row.mx_filesize) }}
</template>
</vxe-column>
<vxe-column width="180" title="操作">
<template #default="{ row }">
<div
style="
display: flex;
align-items: center;
justify-content: center;
height: 28px;
"
>
<el-button
v-if="!props.readonly"
type="danger"
link
@click.stop="handleDeleteOne(row)"
>
删除
</el-button>
<el-button type="primary" link @click.stop="handleDownloadFile(row)">
下载
</el-button>
<el-dropdown v-if="!props.readonly" placement="top" trigger="click">
<el-button type="info" link style="margin-left: 12px"> 更多 </el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleShowUpdateDialog(row)">
修改文件信息
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</vxe-column>
</vxe-table>
</div>
</div>
</div>
</div>
</div>
</div>
</el-splitter-panel>
</el-splitter>
</div>
<template v-if="!props.readonly" #footer>
<el-button type="primary" :disabled="loading" @click="handleClickUpload">
<el-button
type="primary"
:disabled="loading"
@click="handleClickUpload"
v-if="cateId === '1' || cateId === '2'"
>
<el-icon class="el-icon--left">
<G3Icon icon="lucide:mouse-pointer" />
</el-icon>
选择
</el-button>
<el-button type="primary" :disabled="loading" @click="handleSave">
<el-button
type="primary"
:disabled="loading"
@click="handleSave"
v-if="cateId === '1' || cateId === '2'"
>
<el-icon class="el-icon--left">
<G3Icon icon="lucide:upload" />
</el-icon>
@@ -1107,7 +1210,7 @@ function handleTreeNodeClick(data) {
<el-input v-model="updateFile.mx_bz" />
<template v-if="updateFile._status !== 'pending'">
<div style="margin: 4px 0px">是否为箱门照:</div>
<el-switch v-model="updateFile.mx_cate_id" active-value="0" inactive-value="1" />
<el-switch v-model="updateFile._main" :active-value="true" :inactive-value="false" />
</template>
</div>
<template #footer>
@@ -1143,7 +1246,8 @@ function handleTreeNodeClick(data) {
}
.g3-category {
width: 140px;
border-right: 1px solid #ddd;
height: 100%;
}
.g3-file-main {
@@ -1151,6 +1255,7 @@ function handleTreeNodeClick(data) {
padding: 0px 8px;
display: flex;
flex-direction: column;
height: 100%;
}
.g3-main {
@@ -146,21 +146,12 @@ export async function httpUpload(url, file, extraData = {}, config = {}) {
}
async function appendFile(formData, file) {
// ✅ 已经是 File / Blob
if (file instanceof File || file instanceof Blob) {
formData.append("file", file);
return;
}
// ✅ 处理 blob:http://xxx
if (typeof file === "string" && file.startsWith("blob:")) {
const blob = await fetch(file).then((res) => res.blob());
formData.append("file", blob);
return;
}
throw new Error("不支持的文件类型:" + typeof file);
throw new Error("上传只支持 File 或 Blob,不支持字符串");
}
export function reset401Flag() {
@@ -195,7 +195,7 @@ async function downloadAll() {
const containerCountMap = new Map();
for (const item of records) {
const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc");
const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_cate_id asc,mx_xh asc");
const files = (fileRes.success && fileRes.data) || [];
totalFiles += files.length;
@@ -320,7 +320,7 @@ async function downloadSlice() {
containerCountMap.set(containerNo, count + 1);
const folderPromise = loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc");
const folderPromise = loadDataApi("bf_file", `father='${item.b_id}'`, "mx_cate_id asc,mx_xh asc");
allTasks.push(async () => {
const fileRes = await folderPromise;
@@ -403,7 +403,7 @@ async function downloadChunk() {
pageRecords.forEach((item) => {
allPageTasks.push(async () => {
const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc");
const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_cate_id asc,mx_xh asc");
if (!fileRes.success) {
console.error("获取附件失败:", item.b_container_no);
@@ -57,9 +57,13 @@ const fileCategories = [
cateId: "0",
},
{
label: "其它照片",
label: "装货照片",
cateId: "1",
},
{
label: "发货单、签收单、过磅单",
cateId: "2",
},
];
const showPreview = ref(false);
@@ -291,7 +295,7 @@ async function handleDownloadOne(data) {
});
try {
const fileRes = await loadDataApi("bf_file", `father = ${data.b_id}`, "mx_xh asc");
const fileRes = await loadDataApi("bf_file", `father = ${data.b_id}`, "mx_cate_id asc,mx_xh asc");
if (!fileRes.success) {
ElMessage.error("获取附件失败");
@@ -365,7 +369,7 @@ async function handleDownloadBatch() {
const containerCountMap = new Map();
for (const item of records) {
const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_xh asc");
const fileRes = await loadDataApi("bf_file", `father='${item.b_id}'`, "mx_cate_id asc,mx_xh asc");
const files = (fileRes.success && fileRes.data) || [];
totalFiles += files.length;