6d4961f404
2、调整对账 3、解决正式结算冲突bug
1438 lines
48 KiB
Vue
1438 lines
48 KiB
Vue
<template>
|
||
<basic-container class="voucher-manage-page">
|
||
<section class="voucher-manage-page__search">
|
||
<el-form :model="query" label-position="right" label-width="88px">
|
||
<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 v-if="!isCarrier" 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 v-if="!isCarrier" 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 v-if="!isCarrier" prop="uploadSource" label="上传来源" width="100" /><el-table-column
|
||
v-if="!isCarrier"
|
||
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="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="!isCarrier && row.processStatus === '处理完成' && row.auditStatus === '待审核' && hasPermission('vouchermanage_audit')"
|
||
type="primary"
|
||
@click="handleAuditPass(row)"
|
||
>审核通过</el-link
|
||
><el-link
|
||
v-if="!isCarrier && row.processStatus === '处理完成' && row.auditStatus === '待审核' && hasPermission('vouchermanage_audit')"
|
||
type="warning"
|
||
@click="handleAuditReject(row)"
|
||
>审核驳回</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.auditStatus === '审核驳回'"
|
||
type="primary"
|
||
@click="openUpload(row, 'reupload')"
|
||
>重新上传</el-link
|
||
><el-link
|
||
v-if="row.auditStatus === '审核驳回'"
|
||
type="primary"
|
||
@click="openUpload(row, 'changeBatch')"
|
||
>更换运单批次</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="detailVisible"
|
||
title="执行凭证详情"
|
||
width="92%"
|
||
top="5vh"
|
||
class="voucher-detail-dialog"
|
||
destroy-on-close
|
||
>
|
||
<div v-loading="detailLoading" class="voucher-manage-page__detail">
|
||
<div class="voucher-manage-page__detail-summary">
|
||
<div class="voucher-manage-page__detail-field">
|
||
<span class="voucher-manage-page__detail-label">执行凭证号</span>
|
||
<span class="voucher-manage-page__detail-value">{{ detailVoucherNo }}</span>
|
||
</div>
|
||
<div class="voucher-manage-page__detail-field">
|
||
<span class="voucher-manage-page__detail-label">文件夹名称</span>
|
||
<span class="voucher-manage-page__detail-value">{{ detailFolderName }}</span>
|
||
</div>
|
||
<div class="voucher-manage-page__detail-field">
|
||
<span class="voucher-manage-page__detail-label">是否关联运单</span>
|
||
<span class="voucher-manage-page__detail-value">{{ detailRelatedWaybill }}</span>
|
||
</div>
|
||
<div class="voucher-manage-page__detail-field">
|
||
<span class="voucher-manage-page__detail-label">配载单号</span>
|
||
<el-link
|
||
v-if="detailLoadingNo !== '-'"
|
||
class="voucher-manage-page__detail-value voucher-manage-page__detail-link"
|
||
type="primary"
|
||
>
|
||
{{ detailLoadingNo }}
|
||
</el-link>
|
||
<span v-else class="voucher-manage-page__detail-value">-</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="detailImages.length" class="voucher-manage-page__detail-images">
|
||
<el-image
|
||
v-for="(image, index) in detailImages"
|
||
:key="image.url || index"
|
||
class="voucher-manage-page__detail-image"
|
||
:src="image.url"
|
||
:preview-src-list="detailImageUrls"
|
||
:initial-index="index"
|
||
fit="cover"
|
||
preview-teleported
|
||
/>
|
||
</div>
|
||
<div v-if="detailOtherFiles.length" class="voucher-manage-page__detail-files">
|
||
<div v-for="file in detailOtherFiles" :key="file.id || file.url || file.name" class="voucher-manage-page__detail-file">
|
||
<span class="voucher-manage-page__detail-file-name">{{ file.name }}</span>
|
||
<el-link v-if="file.url" type="primary" @click="openDetailFile(file)">查看</el-link>
|
||
</div>
|
||
</div>
|
||
<el-empty v-if="!detailImages.length && !detailOtherFiles.length" description="暂无凭证文件" :image-size="80" />
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
v-model="uploadVisible"
|
||
:title="editing.id ? (uploadMode === 'reupload' ? '重新上传' : '更换运单批次') : '批量导入凭证'"
|
||
width="1200px"
|
||
destroy-on-close
|
||
:before-close="beforeUploadDialogClose"
|
||
>
|
||
<section-card>
|
||
<el-form
|
||
ref="uploadFormRef"
|
||
:model="editing"
|
||
:rules="rules"
|
||
label-position="right"
|
||
label-width="auto"
|
||
class="voucher-manage-page__upload-form"
|
||
>
|
||
<el-form-item v-if="!isCarrier" 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="执行凭证" required
|
||
><el-upload
|
||
action="#"
|
||
accept=".zip,.7z"
|
||
:auto-upload="false"
|
||
:limit="1"
|
||
:on-change="selectVoucherFile"
|
||
:on-remove="clearFile"
|
||
><el-button
|
||
type="primary"
|
||
:loading="uploading"
|
||
:disabled="!editing.projectId || uploading"
|
||
>上传</el-button
|
||
><template #tip
|
||
><span class="voucher-manage-page__upload-tip"
|
||
>请上传车辆的执行凭证压缩包,支持zip、7z格式。 上传附件大于50M时为超大附件,确认提交后可在“批量补录->上传任务”中查看上传进度。</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>
|
||
</section-card>
|
||
|
||
<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="88px"
|
||
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
|
||
highlight-current-row
|
||
@current-change="batchSelection = $event ? [$event] : []"
|
||
><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-column
|
||
label="操作"
|
||
width="100"
|
||
align="center"
|
||
><template #default="{ row }"
|
||
><el-link type="primary" @click="selectBatch(row)">选择</el-link></template
|
||
></el-table-column
|
||
/></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-card class="voucher-manage-page__progress-search">
|
||
<el-form :model="progressQuery" label-position="right" label-width="88px"
|
||
><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-card>
|
||
<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="180" 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, onMounted } from 'vue';
|
||
import { useRouter } from 'vue-router';
|
||
import { useStore } from 'vuex';
|
||
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';
|
||
import SectionCard from '@/components/section-card/main.vue';
|
||
|
||
const router = useRouter();
|
||
const store = useStore();
|
||
|
||
// 权限判断
|
||
const permission = computed(() => store.getters.permission);
|
||
const userInfo = computed(() => store.getters.userInfo);
|
||
const isAdmin = computed(() => userInfo.value?.authority?.includes('admin'));
|
||
const hasPermission = code => {
|
||
if (isAdmin.value) return true;
|
||
return permission.value?.[code] === true;
|
||
};
|
||
|
||
// 判断当前用户是否为承运商(顶级组织是否为"外部组织")
|
||
const isCarrier = ref(false);
|
||
const checkIfCarrier = () => {
|
||
const userInfoData = userInfo.value || {};
|
||
const topDeptName = userInfoData.top_dept_name || userInfoData.topDeptName || '';
|
||
isCarrier.value = topDeptName === '外部组织';
|
||
};
|
||
|
||
onMounted(() => {
|
||
checkIfCarrier();
|
||
});
|
||
|
||
const loading = ref(false),
|
||
rows = ref([]),
|
||
createRange = ref([]),
|
||
searchExpanded = ref(false),
|
||
detailVisible = ref(false),
|
||
detailLoading = ref(false),
|
||
detailRow = ref({}),
|
||
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(),
|
||
selectedVoucherFile = ref(),
|
||
uploadMode = 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, mode = '') => {
|
||
await loadProjects();
|
||
selectedVoucherFile.value = undefined;
|
||
uploadMode.value = mode;
|
||
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: [],
|
||
});
|
||
if (mode === 'reupload') Object.assign(editing, { fileName: '', fileUrl: '', fileTaskId: '' });
|
||
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 = () => {
|
||
selectedVoucherFile.value = undefined;
|
||
Object.assign(editing, { fileName: '', fileUrl: '', fileTaskId: '' });
|
||
};
|
||
const isCurrentUploadTask = taskId =>
|
||
taskId && activeUploadTaskId.value && String(taskId) === String(activeUploadTaskId.value);
|
||
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));
|
||
if (isCurrentUploadTask(currentTaskId)) {
|
||
uploading.value = false;
|
||
activeUploadTaskId.value = undefined;
|
||
}
|
||
try {
|
||
const taskRes = await api.getFileTask(currentTaskId);
|
||
if (taskRes.data?.data?.status === 'completed') return;
|
||
await api.pauseFileTask([currentTaskId]);
|
||
} catch (error) {
|
||
ElMessage.error(error.message || '暂停上传失败');
|
||
}
|
||
};
|
||
const closeUploadDialog = async (pauseUpload = true) => {
|
||
if (closingUploadDialog.value) return;
|
||
closingUploadDialog.value = true;
|
||
try {
|
||
if (pauseUpload) 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 || '';
|
||
if (!editing.fileUrl) throw new Error('未获取到上传文件地址,请稍后重试');
|
||
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);
|
||
return Boolean(editing.fileUrl);
|
||
} catch (error) {
|
||
if (error.name !== 'AbortError')
|
||
ElMessage.error(error.message || '上传中断,可在上传进度中继续上传');
|
||
return false;
|
||
}
|
||
};
|
||
const selectVoucherFile = file => {
|
||
if (!file?.raw) return;
|
||
selectedVoucherFile.value = file.raw;
|
||
editing.fileName = file.raw.name;
|
||
editing.fileUrl = '';
|
||
editing.fileTaskId = '';
|
||
};
|
||
watch(uploadVisible, visible => {
|
||
// 兜底处理:右上角、遮罩、Esc 直接修改 v-model 时仍必须停止上传。
|
||
if (!visible && !closingUploadDialog.value) 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 || [];
|
||
batchPage.total = data.total || 0;
|
||
};
|
||
const resetBatchQuery = () => {
|
||
Object.assign(batchQuery, { batchNo: '', createUser: '', waybillCount: undefined });
|
||
batchCreateRange.value = [];
|
||
batchPage.current = 1;
|
||
loadBatches();
|
||
};
|
||
const selectBatch = row => {
|
||
const batches = new Map();
|
||
selectedBatches.value.forEach(item => {
|
||
if (!batches.has(item.batchNo)) batches.set(item.batchNo, item);
|
||
});
|
||
if (!batches.has(row.batchNo)) batches.set(row.batchNo, row);
|
||
selectedBatches.value = [...batches.values()];
|
||
editing.waybillImportBatchIds = selectedBatches.value.map(item => item.id);
|
||
batchVisible.value = false;
|
||
};
|
||
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.map(item => item.id);
|
||
batchVisible.value = false;
|
||
};
|
||
const removeBatch = row => {
|
||
selectedBatches.value = selectedBatches.value.filter(item => item.id !== row.id);
|
||
editing.waybillImportBatchIds = selectedBatches.value.map(item => item.id);
|
||
};
|
||
const submitUpload = async () => {
|
||
saving.value = true;
|
||
try {
|
||
const valid = await uploadFormRef.value.validate().catch(() => false);
|
||
if (!valid) return;
|
||
if (!editing.fileUrl) {
|
||
if (!selectedVoucherFile.value) {
|
||
ElMessage.warning('请先选择执行凭证');
|
||
return;
|
||
}
|
||
const uploaded = await uploadVoucher({ raw: selectedVoucherFile.value });
|
||
if (!uploaded) return;
|
||
selectedVoucherFile.value = undefined;
|
||
}
|
||
await api.submit({
|
||
...editing,
|
||
waybillImportBatchIds: selectedBatches.value.map(item => item.id),
|
||
});
|
||
ElMessage.success('提交成功');
|
||
await closeUploadDialog(false);
|
||
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 || []).slice().sort((left, right) => {
|
||
const leftTime = left.createTime ? new Date(left.createTime).getTime() : 0;
|
||
const rightTime = right.createTime ? new Date(right.createTime).getTime() : 0;
|
||
if (rightTime !== leftTime) return rightTime - leftTime;
|
||
return String(right.id || '').localeCompare(String(left.id || ''));
|
||
});
|
||
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 => {
|
||
if (row.status === 'completed') return;
|
||
if (isCurrentUploadTask(row.id)) {
|
||
await stopCurrentUpload(row.id);
|
||
} else {
|
||
await api.pauseFileTask([row.id]);
|
||
cancelledTaskIds.value.add(String(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 detailValue = value =>
|
||
value === undefined || value === null || value === '' ? '-' : value;
|
||
const firstDetailValue = (row, keys) => {
|
||
for (const key of keys) {
|
||
const value = row?.[key];
|
||
if (value !== undefined && value !== null && value !== '') return value;
|
||
}
|
||
return undefined;
|
||
};
|
||
const detailVoucherNo = computed(() =>
|
||
detailValue(
|
||
firstDetailValue(detailRow.value, ['voucherNo', 'executeVoucherNo', 'voucherBatchNo'])
|
||
)
|
||
);
|
||
const detailFolderName = computed(() =>
|
||
detailValue(
|
||
firstDetailValue(detailRow.value, [
|
||
'folderName',
|
||
'directoryName',
|
||
'sourceFolderName',
|
||
'voucherFolderName',
|
||
'folder',
|
||
'fileName',
|
||
])
|
||
)
|
||
);
|
||
const detailRelatedWaybill = computed(() => {
|
||
const value = firstDetailValue(detailRow.value, [
|
||
'isRelatedWaybill',
|
||
'isRelateWaybill',
|
||
'isRelationWaybill',
|
||
'relatedWaybill',
|
||
'relatedWaybillFlag',
|
||
'hasRelatedWaybill',
|
||
]);
|
||
if (typeof value === 'boolean') return value ? '是' : '否';
|
||
if (typeof value === 'number') return value > 0 ? '是' : '否';
|
||
if (typeof value === 'string' && value.trim()) {
|
||
if (['1', 'true', 'yes', '是'].includes(value.trim().toLowerCase())) return '是';
|
||
if (['0', 'false', 'no', '否'].includes(value.trim().toLowerCase())) return '否';
|
||
return value;
|
||
}
|
||
const count = firstDetailValue(detailRow.value, ['relatedWaybillCount', 'waybillCount']);
|
||
if (count !== undefined && count !== null && count !== '') {
|
||
return Number(count) > 0 ? '是' : '否';
|
||
}
|
||
return firstDetailValue(detailRow.value, ['waybillBatchNo', 'waybillNo']) ? '是' : '-';
|
||
});
|
||
const detailLoadingNo = computed(() =>
|
||
detailValue(
|
||
firstDetailValue(detailRow.value, [
|
||
'loadingNo',
|
||
'loadingCode',
|
||
'loadBatchNo',
|
||
'loadingBatchNo',
|
||
'stowageNo',
|
||
'stowageCode',
|
||
'stowageBatchNo',
|
||
'shipmentNo',
|
||
'waybillBatchNo',
|
||
'waybillNo',
|
||
])
|
||
)
|
||
);
|
||
const imageFieldKeys = [
|
||
'voucherImages',
|
||
'voucherImageList',
|
||
'executeVoucherImages',
|
||
'executeVoucherImageList',
|
||
'voucherImgList',
|
||
'voucherImageUrls',
|
||
'imageList',
|
||
'imgList',
|
||
'imageUrls',
|
||
'images',
|
||
'pictureList',
|
||
'pictures',
|
||
'photoList',
|
||
'attachments',
|
||
'attachmentList',
|
||
'fileList',
|
||
];
|
||
const fileFieldKeys = ['voucherFiles', 'files', 'fileDetails', 'fileList'];
|
||
const toDetailImageItems = value => {
|
||
if (Array.isArray(value)) return value.flatMap(item => toDetailImageItems(item));
|
||
if (typeof value === 'string') {
|
||
const text = value.trim();
|
||
if (!text) return [];
|
||
try {
|
||
return toDetailImageItems(JSON.parse(text));
|
||
} catch {
|
||
return text.split(/[,\n;]/).map(item => item.trim()).filter(Boolean);
|
||
}
|
||
}
|
||
if (value && typeof value === 'object') {
|
||
const nested = value.records || value.list || value.items || value.data || value.urls;
|
||
if (nested !== undefined) return toDetailImageItems(nested);
|
||
}
|
||
return value && typeof value === 'object' ? [value] : [];
|
||
};
|
||
const normalizeDetailImage = item => {
|
||
if (typeof item === 'string') return { url: item, name: item.split('/').pop() };
|
||
const url =
|
||
item?.url ||
|
||
item?.link ||
|
||
item?.fileUrl ||
|
||
item?.downloadUrl ||
|
||
item?.domain ||
|
||
item?.src ||
|
||
item?.imageUrl ||
|
||
item?.path;
|
||
return url ? { url: String(url), name: item.name || item.fileName || '' } : null;
|
||
};
|
||
const normalizeDetailFile = item => {
|
||
if (typeof item === 'string') {
|
||
return { name: item.split('/').pop() || item, url: item, fileType: '' };
|
||
}
|
||
const url = item?.url || item?.link || item?.fileUrl || item?.downloadUrl || item?.domain || item?.src;
|
||
const name = item?.fileName || item?.name || item?.entryName || item?.imageName || '-';
|
||
return {
|
||
...item,
|
||
id: item?.id,
|
||
name,
|
||
url: url ? String(url) : '',
|
||
fileType: item?.fileType || '',
|
||
contentType: item?.contentType || '',
|
||
};
|
||
};
|
||
const isDetailImage = file =>
|
||
file.fileType === 'image' ||
|
||
String(file.contentType || '').startsWith('image/') ||
|
||
/\.(jpe?g|png|bmp|webp|gif)$/i.test(file.name || '');
|
||
const detailFiles = computed(() => {
|
||
for (const key of fileFieldKeys) {
|
||
const value = detailRow.value?.[key];
|
||
if (value === undefined || value === null || value === '') continue;
|
||
const files = toDetailImageItems(value).map(normalizeDetailFile).filter(Boolean);
|
||
if (files.length) return files;
|
||
}
|
||
return [];
|
||
});
|
||
const detailImages = computed(() => {
|
||
if (detailFiles.value.length) return detailFiles.value.filter(file => isDetailImage(file));
|
||
for (const key of imageFieldKeys) {
|
||
const value = detailRow.value?.[key];
|
||
if (value === undefined || value === null || value === '') continue;
|
||
const images = toDetailImageItems(value).map(normalizeDetailImage).filter(Boolean);
|
||
if (images.length) return images;
|
||
}
|
||
const singleImage = normalizeDetailImage(detailRow.value?.voucherImage || detailRow.value?.image);
|
||
return singleImage ? [singleImage] : [];
|
||
});
|
||
const detailOtherFiles = computed(() =>
|
||
detailFiles.value.filter(file => !isDetailImage(file))
|
||
);
|
||
const detailImageUrls = computed(() => detailImages.value.map(image => image.url));
|
||
const openDetailFile = file => {
|
||
if (file?.url) window.open(file.url, '_blank');
|
||
};
|
||
const view = async row => {
|
||
router.push({
|
||
path: '/business/voucher-manage/detail',
|
||
query: { id: row.id, batchNo: row.voucherBatchNo },
|
||
});
|
||
};
|
||
const download = row => window.open(row.fileUrl, '_blank');
|
||
const handleAuditPass = row =>
|
||
ElMessageBox.confirm(`确认审核通过凭证批次"${row.voucherBatchNo}"吗?`, '提示', {
|
||
type: 'warning',
|
||
}).then(async () => {
|
||
await api.auditPass(row.id);
|
||
ElMessage.success('审核通过');
|
||
load();
|
||
});
|
||
const handleAuditReject = row =>
|
||
ElMessageBox.prompt('请输入驳回原因(可选)', '审核驳回', {
|
||
confirmButtonText: '确认',
|
||
cancelButtonText: '取消',
|
||
inputPattern: /.*/,
|
||
}).then(async ({ value }) => {
|
||
await api.auditReject(row.id, value || '');
|
||
ElMessage.success('审核驳回');
|
||
load();
|
||
});
|
||
load();
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
:global(.voucher-detail-dialog.el-dialog) {
|
||
max-width: 996px;
|
||
border: 1px solid #dcdfe6;
|
||
border-radius: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
:global(.voucher-detail-dialog .el-dialog__header) {
|
||
padding: 20px 24px;
|
||
margin-right: 0;
|
||
//background: #f5f5f5;
|
||
border-bottom: 1px solid #dcdfe6;
|
||
}
|
||
|
||
:global(.voucher-detail-dialog .el-dialog__headerbtn) {
|
||
top: 20px;
|
||
right: 24px;
|
||
}
|
||
|
||
:global(.voucher-detail-dialog .el-dialog__body) {
|
||
max-height: calc(100vh - 130px);
|
||
padding: 0;
|
||
overflow-y: auto;
|
||
background: #fff;
|
||
}
|
||
|
||
.voucher-manage-page {
|
||
&__search {
|
||
padding: 12px 12px 4px;
|
||
margin-bottom: 8px;
|
||
background: #fff;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
}
|
||
&__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: 14px;
|
||
}
|
||
&__upload-tip {
|
||
margin-left: 16px;
|
||
color: #606266;
|
||
}
|
||
&__upload-progress {
|
||
margin-top: 12px;
|
||
:deep(.el-progress__text) {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
:deep(.el-progress-bar__outer) {
|
||
background: #eff1f7;
|
||
border-radius: 6px;
|
||
}
|
||
:deep(.el-progress-bar__inner) {
|
||
border-radius: 6px;
|
||
}
|
||
}
|
||
&__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;
|
||
margin-bottom: 8px;
|
||
background: #fff;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
}
|
||
: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(.voucher-manage-page__batch-search .el-input),
|
||
:deep(.voucher-manage-page__batch-search .el-input-number),
|
||
:deep(.voucher-manage-page__batch-search .el-date-editor) {
|
||
width: 50%;
|
||
}
|
||
: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;
|
||
}
|
||
&__detail {
|
||
min-height: 420px;
|
||
padding: 40px 46px 68px;
|
||
box-sizing: border-box;
|
||
}
|
||
&__detail-summary {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||
column-gap: 56px;
|
||
row-gap: 16px;
|
||
margin-bottom: 32px;
|
||
}
|
||
&__detail-field {
|
||
display: grid;
|
||
grid-template-columns: 140px minmax(0, 1fr);
|
||
column-gap: 16px;
|
||
align-items: center;
|
||
min-height: 24px;
|
||
font-size: 14px;
|
||
line-height: 24px;
|
||
}
|
||
&__detail-label,
|
||
&__detail-value {
|
||
color: #303133;
|
||
white-space: nowrap;
|
||
}
|
||
&__detail-value {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
&__detail-link {
|
||
justify-self: start;
|
||
font-size: inherit;
|
||
line-height: inherit;
|
||
}
|
||
&__detail-images {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, 140px);
|
||
gap: 32px;
|
||
justify-content: start;
|
||
}
|
||
&__detail-image {
|
||
width: 140px;
|
||
height: 100px;
|
||
box-sizing: border-box;
|
||
overflow: hidden;
|
||
cursor: zoom-in;
|
||
background: #fff;
|
||
border: 6px solid #cfcfcf;
|
||
|
||
:deep(.el-image__inner) {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
}
|
||
&__detail-files {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
margin-top: 24px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid #eff1f7;
|
||
}
|
||
&__detail-file {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
min-height: 28px;
|
||
}
|
||
&__detail-file-name {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
color: #303133;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.voucher-manage-page {
|
||
&__detail-images {
|
||
grid-template-columns: repeat(4, 140px);
|
||
}
|
||
}
|
||
}
|
||
|
||
@media (max-width: 680px) {
|
||
.voucher-manage-page {
|
||
&__detail {
|
||
padding: 28px 24px 40px;
|
||
}
|
||
&__detail-summary {
|
||
grid-template-columns: 1fr;
|
||
row-gap: 12px;
|
||
}
|
||
&__detail-images {
|
||
grid-template-columns: repeat(2, minmax(0, 140px));
|
||
gap: 20px;
|
||
}
|
||
}
|
||
}
|
||
</style>
|