1、新增应付明细

2、新增应收明细
3、新增异常处置
4、新增风险处置
5、新增在途追踪
6、修复业务模块bug
This commit is contained in:
2026-08-13 00:01:23 +08:00
parent 748dd80d4a
commit 6554ad80dd
33 changed files with 7868 additions and 1657 deletions
+960
View File
@@ -0,0 +1,960 @@
<template>
<basic-container class="voucher-manage-page">
<section class="voucher-manage-page__search">
<el-form :model="query" label-position="right" label-width="160px">
<div class="voucher-manage-page__search-grid">
<el-form-item label="凭证批次号"
><el-input v-model="query.voucherBatchNo" clearable placeholder="请输入"
/></el-form-item>
<el-form-item label="审核状态"
><el-select v-model="query.auditStatus" clearable placeholder="全部"
><el-option label="待审核" value="待审核" /><el-option
label="审核通过"
value="审核通过" /><el-option label="审核驳回" value="审核驳回" /></el-select
></el-form-item>
<el-form-item label="处理状态"
><el-select v-model="query.processStatus" clearable placeholder="全部"
><el-option label="上传中" value="上传中" /><el-option
label="处理中"
value="处理中" /><el-option label="处理完成" value="处理完成" /></el-select
></el-form-item>
<el-form-item label="运单批次号"
><el-input v-model="query.waybillBatchNo" clearable placeholder="请输入"
/></el-form-item>
<template v-if="searchExpanded">
<el-form-item label="上传来源"
><el-select v-model="query.uploadSource" clearable placeholder="全部"
><el-option label="内部" value="内部" /><el-option
label="承运商"
value="承运商" /></el-select
></el-form-item>
<el-form-item label="承运商"
><el-input v-model="query.carrierName" clearable placeholder="请输入"
/></el-form-item>
<el-form-item label="创建日期"
><el-date-picker
v-model="createRange"
type="daterange"
value-format="YYYY-MM-DD"
range-separator="-"
start-placeholder="请选择"
end-placeholder="请选择"
/></el-form-item>
</template>
<div class="voucher-manage-page__search-actions">
<el-button type="primary" @click="search">查询</el-button
><el-button @click="reset">重置</el-button
><el-link type="primary" @click="searchExpanded = !searchExpanded">{{
searchExpanded ? '收起' : '展开'
}}</el-link>
</div>
</div>
</el-form>
</section>
<section>
<div class="voucher-manage-page__toolbar">
<el-button type="primary" @click="openUpload">批量导入凭证</el-button
><el-button plain @click="openProgress">上传进度</el-button>
</div>
<el-table v-loading="loading" :data="rows" border class="voucher-manage-page__table">
<el-table-column type="selection" width="52" fixed="left" align="center" /><el-table-column
type="index"
label="序号"
width="58"
fixed="left"
align="center"
/>
<el-table-column prop="voucherBatchNo" label="凭证批次号" min-width="150" fixed="left"
><template #default="{ row }"
><el-link class="voucher-manage-page__link" type="primary" @click="view(row)">{{
row.voucherBatchNo
}}</el-link></template
></el-table-column
>
<el-table-column prop="waybillBatchNo" label="运单批次号" min-width="170" /><el-table-column
prop="fileName"
label="文件名"
min-width="200"
show-overflow-tooltip
/>
<el-table-column prop="uploadSource" label="上传来源" width="100" /><el-table-column
prop="carrierName"
label="承运商名称"
min-width="160"
><template #default="{ row }">{{ row.carrierName || '-' }}</template></el-table-column
>
<el-table-column prop="processStatus" label="处理状态" width="120" /><el-table-column
prop="voucherCount"
label="凭证数量"
width="100"
/><el-table-column
prop="relatedWaybillCount"
label="已关联运单"
width="115"
/><el-table-column prop="unRelatedWaybillCount" label="未关联运单" width="115" />
<el-table-column prop="status" label="状态" width="100"
><template #default="{ row }">{{
Number(row.status) === 1 ? '启用' : '停用'
}}</template></el-table-column
><el-table-column prop="createTime" label="创建时间" min-width="170" />
<el-table-column
prop="auditStatus"
label="审核状态"
width="120"
fixed="right"
align="center"
/><el-table-column label="操作" width="320" fixed="right" align="left"
><template #default="{ row }"
><div class="voucher-manage-page__actions">
<el-link
v-if="row.processStatus === '处理完成' && row.auditStatus === '待审核'"
type="primary"
>流程</el-link
><el-link v-if="row.processStatus === '处理完成'" type="primary" @click="view(row)"
>查看</el-link
><el-link
v-if="row.processStatus === '处理完成'"
type="primary"
@click="download(row)"
>下载</el-link
><el-link
v-if="row.processStatus !== '处理完成' || row.auditStatus === '待审核'"
type="primary"
@click="openUpload(row)"
>更换运单批次</el-link
><el-link
v-if="row.processStatus === '上传中' || row.auditStatus === '审核驳回'"
type="danger"
@click="removeRow(row)"
>删除</el-link
>
</div></template
></el-table-column
>
</el-table>
<div class="voucher-manage-page__pagination">
<el-pagination
v-model:current-page="page.current"
v-model:page-size="page.size"
:total="page.total"
:page-sizes="[10, 20, 50]"
layout="total, prev, pager, next, sizes"
@current-change="load"
@size-change="load"
/>
</div>
</section>
</basic-container>
<el-dialog
v-model="uploadVisible"
:title="editing.id ? '更换运单批次' : '批量导入凭证'"
width="1200px"
destroy-on-close
:before-close="beforeUploadDialogClose"
>
<el-form
ref="uploadFormRef"
:model="editing"
:rules="rules"
label-position="right"
label-width="auto"
class="voucher-manage-page__upload-form"
>
<el-form-item label="项目名称" prop="projectId"
><el-select
v-model="editing.projectId"
class="voucher-manage-page__project-select"
clearable
filterable
placeholder="请选择"
@change="changeProject"
><el-option
v-for="item in projectOptions"
:key="item.id"
:label="item.projectName"
:value="item.id" /></el-select
></el-form-item>
<el-form-item label="执行凭证" prop="fileUrl"
><el-upload
action="#"
accept=".zip,.7z"
:auto-upload="false"
:limit="1"
:on-change="uploadVoucher"
:on-remove="clearFile"
><el-button
type="primary"
:loading="uploading"
:disabled="!editing.projectId || uploading"
>上传</el-button
><template #tip
><span class="voucher-manage-page__upload-tip"
>支持 zip7z 格式采用分片上传上传中断后可在上传进度继续上传</span
></template
></el-upload
><el-progress
v-if="uploading || editing.fileTaskId"
:percentage="uploadPercent"
:status="uploadPercent === 100 ? 'success' : undefined"
class="voucher-manage-page__upload-progress"
/></el-form-item>
<el-form-item label="关联运输批次" prop="waybillImportBatchIds"
><el-button type="primary" @click="openBatchDialog">选择</el-button></el-form-item
>
</el-form>
<el-table :data="selectedBatches" border
><el-table-column type="index" label="序号" width="80" /><el-table-column
prop="batchNo"
label="运输批次号"
min-width="220"
/><el-table-column prop="createTime" label="创建时间" min-width="170" /><el-table-column
prop="waybillCount"
label="运单数"
width="120"
/><el-table-column prop="createUserName" label="创建人" min-width="140" /><el-table-column
label="操作"
width="100"
><template #default="{ row }"
><el-link type="danger" @click="removeBatch(row)">删除</el-link></template
></el-table-column
></el-table
>
<template #footer
><el-button @click="closeUploadDialog">取消</el-button
><el-button type="primary" :loading="saving" @click="submitUpload"
>确认上传</el-button
></template
>
</el-dialog>
<el-dialog v-model="batchVisible" title="关联运单批次" width="1200px" destroy-on-close>
<el-form
:model="batchQuery"
label-position="right"
label-width="160px"
class="voucher-manage-page__batch-search"
><el-row :gutter="20"
><el-col :span="12"
><el-form-item label="运单批次号"
><el-input
v-model="batchQuery.batchNo"
clearable
placeholder="请输入" /></el-form-item></el-col
><el-col :span="12"
><el-form-item label="创建日期"
><el-date-picker
v-model="batchCreateRange"
type="daterange"
value-format="YYYY-MM-DD"
range-separator="-" /></el-form-item></el-col
><el-col :span="12"
><el-form-item label="创建人"
><el-input
v-model="batchQuery.createUser"
clearable
placeholder="请输入" /></el-form-item></el-col
><el-col :span="12"
><el-form-item label="运单数"
><el-input-number v-model="batchQuery.waybillCount" :min="0" /></el-form-item></el-col
></el-row>
<div class="voucher-manage-page__search-actions">
<el-button type="primary" @click="loadBatches">查询</el-button
><el-button @click="resetBatchQuery">重置</el-button>
</div></el-form
>
<el-table
ref="batchTableRef"
:data="batchRows"
border
@selection-change="batchSelection = $event"
><el-table-column type="selection" width="56" /><el-table-column
type="index"
label="序号"
width="80" /><el-table-column
prop="batchNo"
label="运单批次号"
min-width="240" /><el-table-column
prop="createTime"
label="创建时间"
min-width="180" /><el-table-column label="运单数" width="120"
><template #default="{ row }">{{ row.waybillCount ?? '-' }}</template></el-table-column
><el-table-column prop="createUserName" label="创建人" min-width="140"
/></el-table>
<div class="voucher-manage-page__pagination">
<el-pagination
v-model:current-page="batchPage.current"
v-model:page-size="batchPage.size"
:total="batchPage.total"
:page-sizes="[10, 20, 50]"
layout="total, prev, pager, next, sizes"
@current-change="loadBatches"
@size-change="loadBatches"
/>
</div>
<template #footer
><el-button @click="batchVisible = false">取消</el-button
><el-button type="primary" @click="confirmBatches">确认</el-button></template
>
</el-dialog>
<el-dialog
v-model="progressVisible"
title="上传进度"
width="1200px"
destroy-on-close
:before-close="beforeProgressDialogClose"
@opened="loadProgress"
>
<section class="voucher-manage-page__progress-search">
<el-form :model="progressQuery" label-position="right" label-width="160px"
><div class="voucher-manage-page__search-grid">
<el-form-item label="凭证批次号"
><el-input
v-model="progressQuery.businessId"
clearable
placeholder="请输入" /></el-form-item
><el-form-item label="文件名称"
><el-input
v-model="progressQuery.attachmentName"
clearable
placeholder="请输入" /></el-form-item
><el-form-item label="上传状态"
><el-select v-model="progressQuery.status" clearable placeholder="全部"
><el-option label="正在上传" value="uploading" /><el-option
label="已暂停"
value="paused" /><el-option label="上传完成" value="completed" /><el-option
label="上传失败"
value="failed" /></el-select
></el-form-item>
<div class="voucher-manage-page__search-actions">
<el-button type="primary" @click="searchProgress">查询</el-button
><el-button @click="resetProgress">重置</el-button>
</div>
</div></el-form
>
</section>
<el-table :data="progressRows" border class="voucher-manage-page__table"
><el-table-column type="selection" width="52" /><el-table-column
type="index"
label="序号"
width="58"
/><el-table-column prop="businessId" label="凭证批次号" min-width="160" /><el-table-column
prop="attachmentName"
label="文件名称"
min-width="250"
show-overflow-tooltip
/><el-table-column prop="suffix" label="文件类型" width="100" /><el-table-column
label="文件大小"
width="120"
><template #default="{ row }">{{ formatSize(row.size) }}</template></el-table-column
><el-table-column prop="createUserName" label="上传人" width="120" /><el-table-column
prop="createTime"
label="上传时间"
min-width="170"
/><el-table-column label="上传进度" min-width="220"
><template #default="{ row }"
><div class="voucher-manage-page__progress-cell">
<el-progress
:percentage="taskPercent(row)"
:stroke-width="10"
:show-text="false"
color="#409eff"
/><span class="voucher-manage-page__progress-value">{{ taskPercent(row) }}%</span>
</div></template
></el-table-column
><el-table-column label="上传状态" width="120"
><template #default="{ row }">{{ taskStatusName(row.status) }}</template></el-table-column
><el-table-column label="操作" width="160" fixed="right"
><template #default="{ row }"
><el-link
v-if="row.status === 'paused' || row.status === 'failed'"
type="primary"
@click="resumeTask(row)"
>继续上传</el-link
><el-link v-if="row.status === 'uploading'" type="danger" @click="pauseTask(row)"
>取消</el-link
></template
></el-table-column
></el-table
>
<div class="voucher-manage-page__pagination">
<el-pagination
v-model:current-page="progressPage.current"
v-model:page-size="progressPage.size"
:total="progressPage.total"
:page-sizes="[10, 20, 50]"
layout="total, prev, pager, next, sizes"
@current-change="loadProgress"
@size-change="loadProgress"
/>
</div>
</el-dialog>
<input
ref="resumeFileInput"
class="voucher-manage-page__hidden-file"
type="file"
accept=".zip,.7z"
@change="resumeFileSelected"
/>
</template>
<script setup>
import { computed, reactive, ref, watch } from 'vue';
import md5 from 'js-md5';
import { ElMessage, ElMessageBox } from 'element-plus';
import { getList as getProjectList } from '@/api/business/project-apply';
import * as api from '@/api/business/voucher-manage';
const loading = ref(false),
rows = ref([]),
createRange = ref([]),
searchExpanded = ref(false),
uploadVisible = ref(false),
batchVisible = ref(false),
progressVisible = ref(false),
uploadFormRef = ref(),
uploading = ref(false),
saving = ref(false),
uploadPercent = ref(0),
resumeFileInput = ref(),
resumeTarget = ref(),
uploadAbortController = ref(),
uploadSession = ref(0),
closingUploadDialog = ref(false),
closingProgressDialog = ref(false),
cancelledTaskIds = ref(new Set()),
uploadCancelled = ref(false),
activeUploadTaskId = ref();
const query = reactive({
voucherBatchNo: '',
auditStatus: '',
processStatus: '',
waybillBatchNo: '',
uploadSource: '',
carrierName: '',
});
const page = reactive({ current: 1, size: 10, total: 0 });
const editing = reactive({
id: '',
voucherBatchNo: '',
projectId: '',
projectName: '',
fileName: '',
fileUrl: '',
fileTaskId: '',
waybillImportBatchIds: [],
});
const projectOptions = ref([]);
const selectedBatches = ref([]),
batchRows = ref([]),
batchSelection = ref([]),
batchCreateRange = ref([]);
const batchQuery = reactive({ batchNo: '', createUser: '', waybillCount: undefined });
const batchPage = reactive({ current: 1, size: 10, total: 0 });
const progressQuery = reactive({ businessId: '', attachmentName: '', status: '' });
const progressPage = reactive({ current: 1, size: 10, total: 0 });
const progressRows = ref([]);
const rules = {
projectId: [{ required: true, message: '请选择项目名称' }],
waybillImportBatchIds: [{ required: true, message: '请关联运输批次' }],
};
const load = async () => {
loading.value = true;
try {
const res = await api.getList(page.current, page.size, {
...query,
createTimeStart: createRange.value?.[0] ? `${createRange.value[0]} 00:00:00` : '',
createTimeEnd: createRange.value?.[1] ? `${createRange.value[1]} 23:59:59` : '',
});
const data = res.data?.data || {};
rows.value = data.records || [];
page.total = data.total || 0;
} finally {
loading.value = false;
}
};
const search = () => {
page.current = 1;
load();
};
const reset = () => {
Object.assign(query, {
voucherBatchNo: '',
auditStatus: '',
processStatus: '',
waybillBatchNo: '',
uploadSource: '',
carrierName: '',
});
createRange.value = [];
search();
};
const loadProjects = async () => {
const res = await getProjectList(1, 9999, { temporaryCreditLimitSelectable: true });
projectOptions.value = res.data?.data?.records || [];
};
const changeProject = projectId => {
editing.projectName = projectOptions.value.find(item => item.id === projectId)?.projectName || '';
};
const openUpload = async row => {
await loadProjects();
Object.assign(editing, {
id: row?.id || '',
voucherBatchNo: row?.voucherBatchNo || '',
projectId: row?.projectId || '',
projectName: row?.projectName || '',
fileName: row?.fileName || '',
fileUrl: row?.fileUrl || '',
fileTaskId: row?.fileTaskId || '',
waybillImportBatchIds: [],
});
uploadPercent.value = 0;
selectedBatches.value = [];
uploadVisible.value = true;
if (row?.waybillBatchNo) {
await loadBatches();
selectedBatches.value = batchRows.value.filter(item =>
row.waybillBatchNo.split(',').includes(item.batchNo)
);
editing.waybillImportBatchIds = selectedBatches.value.map(item => item.id);
}
};
const clearFile = () => Object.assign(editing, { fileName: '', fileUrl: '', fileTaskId: '' });
const stopCurrentUpload = async taskId => {
// 先使整个上传会话失效,阻止 MD5 计算、创建任务等异步流程返回后继续上传。
uploadCancelled.value = true;
uploadSession.value += 1;
uploadAbortController.value?.abort();
const currentTaskId = taskId || activeUploadTaskId.value || editing.fileTaskId;
if (!currentTaskId) return;
cancelledTaskIds.value.add(String(currentTaskId));
try {
await api.pauseFileTask([currentTaskId]);
} catch (error) {
ElMessage.error(error.message || '暂停上传失败');
}
};
const closeUploadDialog = async () => {
if (closingUploadDialog.value) return;
closingUploadDialog.value = true;
try {
await stopCurrentUpload(editing.fileTaskId);
uploadVisible.value = false;
} finally {
closingUploadDialog.value = false;
}
};
const beforeUploadDialogClose = async done => {
if (closingUploadDialog.value) {
done();
return;
}
closingUploadDialog.value = true;
try {
await stopCurrentUpload(editing.fileTaskId);
done();
} finally {
closingUploadDialog.value = false;
}
};
const beforeProgressDialogClose = async done => {
if (closingProgressDialog.value) return;
closingProgressDialog.value = true;
try {
if (uploading.value || activeUploadTaskId.value) {
await stopCurrentUpload(activeUploadTaskId.value);
}
done();
} finally {
closingProgressDialog.value = false;
}
};
const ensureUploadSession = (sessionId, taskId) => {
if (
uploadCancelled.value ||
sessionId !== uploadSession.value ||
(taskId && cancelledTaskIds.value.has(String(taskId)))
) {
throw new DOMException('上传已暂停', 'AbortError');
}
};
const calculateMd5 = async file => {
const buffer = await file.arrayBuffer();
return md5.base64(buffer);
};
const uploadParts = async (file, task, sessionId) => {
uploading.value = true;
activeUploadTaskId.value = task.id;
const abortController = new AbortController();
uploadAbortController.value = abortController;
const chunkSize = task.chunkSize;
try {
for (
let partNumber = Math.max(1, (task.currentIndex || 0) + 1);
partNumber <= task.chunkTotal;
partNumber += 1
) {
ensureUploadSession(sessionId, task.id);
if (abortController.signal.aborted) throw new DOMException('上传已暂停', 'AbortError');
const urlRes = await api.getPartUploadUrl(task.id, partNumber);
ensureUploadSession(sessionId, task.id);
if (abortController.signal.aborted) throw new DOMException('上传已暂停', 'AbortError');
const url = urlRes.data?.data;
const part = file.slice(
(partNumber - 1) * chunkSize,
Math.min(partNumber * chunkSize, file.size)
);
const response = await fetch(url, {
method: 'PUT',
body: part,
signal: abortController.signal,
});
if (!response.ok) throw new Error(`分片${partNumber}上传失败`);
const etag = String(response.headers.get('etag') || '').replaceAll('"', '');
if (!etag) throw new Error('MinIO 未返回分片 ETag,请确认跨域配置已暴露 ETag 响应头');
await api.updateFileTask({ id: task.id, partNumber, etag });
ensureUploadSession(sessionId, task.id);
if (abortController.signal.aborted) throw new DOMException('上传已暂停', 'AbortError');
uploadPercent.value = Math.round((partNumber / task.chunkTotal) * 100);
if (progressVisible.value) await loadProgress();
}
ensureUploadSession(sessionId, task.id);
const fileUrlRes = await api.getFileTaskUrl(task.id);
ensureUploadSession(sessionId, task.id);
editing.fileName = file.name;
editing.fileTaskId = task.id;
editing.fileUrl = fileUrlRes.data?.data || '';
await api.completeUploadFile({
voucherBatchNo: task.businessId,
fileTaskId: task.id,
fileName: file.name,
fileUrl: editing.fileUrl,
});
ElMessage.success('执行凭证上传完成');
} catch (error) {
if (error.name !== 'AbortError')
ElMessage.error(error.message || '上传中断,可在上传进度中继续上传');
throw error;
} finally {
uploading.value = false;
if (uploadAbortController.value === abortController) uploadAbortController.value = undefined;
if (activeUploadTaskId.value === task.id) activeUploadTaskId.value = undefined;
if (progressVisible.value) await loadProgress();
}
};
const ensureUploadDraft = async fileName => {
if (editing.id) return;
if (!editing.projectId) throw new Error('请先选择项目名称');
const res = await api.createUploadDraft({ projectId: editing.projectId, fileName });
const draft = res.data?.data;
if (!draft?.id || !draft?.voucherBatchNo) throw new Error('创建凭证上传记录失败');
Object.assign(editing, {
id: draft.id,
voucherBatchNo: draft.voucherBatchNo,
projectName: draft.projectName,
});
};
const uploadVoucher = async file => {
const raw = file.raw;
if (!raw) return;
uploadCancelled.value = false;
const sessionId = uploadSession.value + 1;
uploadSession.value = sessionId;
try {
if (!resumeTarget.value) await ensureUploadDraft(raw.name);
ensureUploadSession(sessionId);
const md5 = await calculateMd5(raw);
ensureUploadSession(sessionId);
const resumeTask = resumeTarget.value;
let task;
if (resumeTask) {
if (
md5 !== resumeTask.md5 ||
raw.name !== resumeTask.attachmentName ||
raw.size !== Number(resumeTask.size)
) {
ElMessage.error('请选择与上传任务相同的文件');
return;
}
const taskRes = await api.getFileTask(resumeTask.id);
ensureUploadSession(sessionId);
task = taskRes.data?.data;
if (!task || String(task.id) !== String(resumeTask.id)) {
ElMessage.error('上传任务不存在或已失效');
return;
}
await api.resumeFileTask(task.id);
ensureUploadSession(sessionId);
task.status = 'uploading';
if (progressVisible.value) await loadProgress();
} else {
const existingRes = await api.queryFileTask(md5);
ensureUploadSession(sessionId);
task = existingRes.data?.data;
}
const chunkSize = 5 * 1024 * 1024;
if (
!task ||
task.status === 'completed' ||
(!resumeTask && task.businessId !== editing.voucherBatchNo)
) {
const createRes = await api.createFileTask({
md5,
attachmentName: raw.name,
size: raw.size,
chunkSize,
chunkTotal: Math.ceil(raw.size / chunkSize),
force: !!task,
businessType: 'voucher',
businessId: editing.voucherBatchNo,
});
ensureUploadSession(sessionId);
task = createRes.data?.data;
}
editing.fileName = raw.name;
editing.fileTaskId = task.id;
activeUploadTaskId.value = task.id;
cancelledTaskIds.value.delete(String(task.id));
resumeTarget.value = undefined;
await uploadParts(raw, task, sessionId);
} catch (error) {
if (error.name !== 'AbortError')
ElMessage.error(error.message || '上传中断,可在上传进度中继续上传');
}
};
watch(uploadVisible, visible => {
// 兜底处理:右上角、遮罩、Esc 直接修改 v-model 时仍必须停止上传。
if (!visible) void stopCurrentUpload(editing.fileTaskId);
});
const openBatchDialog = async () => {
batchVisible.value = true;
await loadBatches();
};
const loadBatches = async () => {
const res = await api.getWaybillBatches(batchPage.current, batchPage.size, {
batchNo: batchQuery.batchNo,
createUser: batchQuery.createUser,
waybillCount: batchQuery.waybillCount,
createTimeStart: batchCreateRange.value?.[0] ? `${batchCreateRange.value[0]} 00:00:00` : '',
createTimeEnd: batchCreateRange.value?.[1] ? `${batchCreateRange.value[1]} 23:59:59` : '',
});
const data = res.data?.data || {};
batchRows.value = (data.records || []).map(item => ({ ...item, id: item.waybillIds }));
batchPage.total = data.total || 0;
};
const resetBatchQuery = () => {
Object.assign(batchQuery, { batchNo: '', createUser: '', waybillCount: undefined });
batchCreateRange.value = [];
batchPage.current = 1;
loadBatches();
};
const confirmBatches = () => {
const batches = new Map();
batchSelection.value.forEach(item => {
if (!batches.has(item.batchNo)) batches.set(item.batchNo, item);
});
selectedBatches.value = [...batches.values()];
editing.waybillImportBatchIds = selectedBatches.value.flatMap(item =>
String(item.waybillIds || item.id)
.split(',')
.filter(Boolean)
);
batchVisible.value = false;
};
const removeBatch = row => {
selectedBatches.value = selectedBatches.value.filter(item => item.id !== row.id);
editing.waybillImportBatchIds = selectedBatches.value.flatMap(item =>
String(item.waybillIds || item.id)
.split(',')
.filter(Boolean)
);
};
const submitUpload = async () => {
saving.value = true;
try {
if (uploading.value && editing.fileTaskId) {
await api.pauseFileTask([editing.fileTaskId]);
uploadAbortController.value?.abort();
}
const valid = await uploadFormRef.value.validate().catch(() => false);
if (!valid) return;
if (!editing.fileUrl && !editing.fileTaskId) {
ElMessage.warning('请先选择执行凭证');
return;
}
await api.submit({
...editing,
waybillImportBatchIds: selectedBatches.value.flatMap(item =>
String(item.waybillIds || item.id)
.split(',')
.filter(Boolean)
),
});
ElMessage.success('提交成功');
await closeUploadDialog();
load();
} finally {
saving.value = false;
}
};
const openProgress = () => {
progressVisible.value = true;
};
const loadProgress = async () => {
const res = await api.getFileTaskList(progressPage.current, progressPage.size, {
...progressQuery,
businessType: 'voucher',
});
const data = res.data?.data || {};
progressRows.value = data.records || [];
progressPage.total = data.total || 0;
};
const searchProgress = () => {
progressPage.current = 1;
loadProgress();
};
const resetProgress = () => {
Object.assign(progressQuery, { businessId: '', attachmentName: '', status: '' });
searchProgress();
};
const taskPercent = row =>
row.chunkTotal ? Math.round(((row.currentIndex || 0) / row.chunkTotal) * 100) : 0;
const taskStatusName = status =>
({ uploading: '正在上传', paused: '已暂停', completed: '上传完成', failed: '上传失败' }[status] ||
status);
const formatSize = size =>
size >= 1024 ** 3 ? `${(size / 1024 ** 3).toFixed(1)}G` : `${(size / 1024 ** 2).toFixed(1)}M`;
const pauseTask = async row => {
await api.pauseFileTask([row.id]);
ElMessage.success('已取消上传');
loadProgress();
};
const resumeTask = row => {
resumeTarget.value = row;
activeUploadTaskId.value = row.id;
resumeFileInput.value?.click();
};
const resumeFileSelected = event => {
const [raw] = event.target.files || [];
if (raw) uploadVoucher({ raw });
event.target.value = '';
};
const removeRow = row =>
ElMessageBox.confirm(`确认删除凭证批次“${row.voucherBatchNo}”吗?`, '提示', {
type: 'warning',
}).then(async () => {
await api.remove(row.id);
ElMessage.success('删除成功');
load();
});
const view = row => ElMessage.info(`凭证批次:${row.voucherBatchNo}`);
const download = row => window.open(row.fileUrl, '_blank');
load();
</script>
<style lang="scss" scoped>
.voucher-manage-page {
&__search {
padding: 12px 12px 4px;
margin-bottom: 8px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
&__search-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px 24px;
}
&__search-actions {
display: flex;
grid-column: 1 / -1;
justify-content: flex-end;
align-items: center;
gap: 8px;
min-height: 32px;
}
&__toolbar {
display: flex;
align-items: center;
gap: 12px;
margin: 12px 0;
}
&__table {
width: 100%;
}
&__link {
padding: 0;
min-height: auto;
}
&__actions {
display: flex;
flex-wrap: wrap;
gap: 2px 12px;
}
&__pagination {
display: flex;
justify-content: flex-end;
padding: 12px 0 0;
background: #fff;
}
&__project-select {
width: 280px;
}
&__hidden-file {
display: none;
}
&__upload-form :deep(.el-form-item) {
margin-bottom: 16px;
}
&__upload-tip {
margin-left: 16px;
color: #606266;
}
&__progress-cell {
display: flex;
align-items: center;
gap: 8px;
:deep(.el-progress) {
flex: 1;
}
:deep(.el-progress-bar__outer) {
background: #eff1f7;
border-radius: 6px;
}
}
&__progress-value {
min-width: 38px;
color: #303133;
text-align: right;
}
&__batch-search {
padding: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-bottom: 8px;
}
:deep(.el-form-item) {
margin-bottom: 8px;
}
:deep(.el-form-item__label) {
white-space: nowrap;
}
:deep(.el-input),
:deep(.el-select),
:deep(.el-date-editor.el-input),
:deep(.el-date-editor.el-input__wrapper),
:deep(.el-date-editor--daterange) {
width: 100%;
}
:deep(.voucher-manage-page__project-select) {
width: 280px;
}
:deep(.el-table) {
--el-table-border-color: #eff1f7;
--el-table-row-hover-bg-color: #f5f7fa;
}
:deep(.el-table__body tr:nth-child(even) > td) {
background: #fafafa;
}
}
</style>