1337 lines
36 KiB
Vue
1337 lines
36 KiB
Vue
<template>
|
||
<div class="upload-data-container">
|
||
<!-- 主内容区 -->
|
||
<div class="main-content">
|
||
<div v-if="isProjectTagging || isProjectArchived" class="tagging-lock-tip">
|
||
{{ isProjectArchived ? "项目已归档,暂不可上传或拉取数据。" : "项目正在进行银行流水打标,暂不可上传或拉取数据。" }}
|
||
</div>
|
||
|
||
<!-- 文件上传记录列表 -->
|
||
<div class="file-list-section">
|
||
<div class="list-toolbar">
|
||
<el-button icon="el-icon-refresh" @click="handleManualRefresh">刷新</el-button>
|
||
<div class="toolbar-actions">
|
||
<el-button
|
||
size="small"
|
||
icon="el-icon-upload2"
|
||
:disabled="isProjectTagging || isProjectArchived"
|
||
@click="handleOpenBatchUploadDialog"
|
||
>
|
||
上传流水
|
||
</el-button>
|
||
<el-button
|
||
size="small"
|
||
icon="el-icon-download"
|
||
:disabled="isProjectTagging || isProjectArchived"
|
||
@click="handleFetchBankInfo"
|
||
>
|
||
拉取本行信息
|
||
</el-button>
|
||
<el-button
|
||
size="small"
|
||
icon="el-icon-upload2"
|
||
:disabled="isProjectTagging || isProjectArchived"
|
||
@click="handleGoCreditInfoPage"
|
||
>
|
||
征信导入
|
||
</el-button>
|
||
<el-button
|
||
size="small"
|
||
type="primary"
|
||
icon="el-icon-view"
|
||
:disabled="isReportDisabled"
|
||
@click="handleViewReport"
|
||
>
|
||
查看报告
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-table :data="fileUploadList" v-loading="listLoading">
|
||
<el-table-column prop="fileName" label="文件名" min-width="200"></el-table-column>
|
||
<el-table-column prop="fileSize" label="文件大小" width="120">
|
||
<template slot-scope="scope">
|
||
{{ formatFileSize(scope.row.fileSize) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="fileStatus" label="状态" width="120">
|
||
<template slot-scope="scope">
|
||
<el-tag :type="getStatusType(scope.row.fileStatus)" size="small">
|
||
{{ getStatusText(scope.row.fileStatus) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="enterpriseNames" label="主体名称" min-width="150">
|
||
<template slot-scope="scope">
|
||
{{ scope.row.enterpriseNames || '-' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="accountNos" label="主体账号" min-width="180">
|
||
<template slot-scope="scope">
|
||
{{ scope.row.accountNos || '-' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="sourceProjectName" label="来源" min-width="180">
|
||
<template slot-scope="scope">
|
||
<span v-if="scope.row.sourceType === 'HISTORY_IMPORT'">
|
||
历史导入 · {{ scope.row.sourceProjectName || '-' }}
|
||
</span>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="uploadTime" label="上传时间" width="180">
|
||
<template slot-scope="scope">
|
||
{{ formatUploadTime(scope.row.uploadTime) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="uploadUser" label="上传人" width="100"></el-table-column>
|
||
<el-table-column label="操作" width="160" fixed="right">
|
||
<template slot-scope="scope">
|
||
<el-button
|
||
v-if="getRowAction(scope.row)"
|
||
type="text"
|
||
@click="handleRowAction(scope.row)"
|
||
>
|
||
{{ getRowAction(scope.row).text }}
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<el-pagination
|
||
@current-change="handlePageChange"
|
||
:current-page="queryParams.pageNum"
|
||
:page-size="queryParams.pageSize"
|
||
:total="total"
|
||
layout="total, prev, pager, next, jumper"
|
||
style="margin-top: 16px; text-align: right"
|
||
></el-pagination>
|
||
</div>
|
||
</div>
|
||
|
||
<el-dialog
|
||
title="拉取本行信息"
|
||
:visible.sync="pullBankInfoDialogVisible"
|
||
:close-on-click-modal="false"
|
||
width="640px"
|
||
>
|
||
<el-form
|
||
class="pull-bank-info-form"
|
||
:model="pullBankInfoForm"
|
||
label-width="100px"
|
||
>
|
||
<el-form-item label="证件号码">
|
||
<el-input
|
||
v-model="pullBankInfoForm.idCardText"
|
||
type="textarea"
|
||
:rows="5"
|
||
:disabled="isProjectTagging || isProjectArchived"
|
||
placeholder="仅支持英文逗号分隔"
|
||
/>
|
||
<div class="pull-bank-field-tip">
|
||
仅支持英文逗号分隔,文件解析结果会自动合并并去重
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="身份证文件">
|
||
<div class="pull-bank-file-panel">
|
||
<div class="pull-bank-file-actions">
|
||
<el-upload
|
||
class="pull-bank-file-upload"
|
||
action="#"
|
||
:auto-upload="false"
|
||
:limit="1"
|
||
:show-file-list="false"
|
||
:file-list="idCardFileList"
|
||
accept=".xls,.xlsx"
|
||
:disabled="isProjectTagging || isProjectArchived"
|
||
:on-change="handleIdCardFileChange"
|
||
:on-remove="handleIdCardFileRemove"
|
||
>
|
||
<el-button slot="trigger" size="small" type="primary" plain>
|
||
选择文件
|
||
</el-button>
|
||
</el-upload>
|
||
<div class="pull-bank-file-tip">
|
||
支持 .xls、.xlsx 文件,解析后自动补充证件号码
|
||
</div>
|
||
</div>
|
||
<div v-if="idCardFileList.length > 0" class="selected-id-card-file">
|
||
<div class="selected-id-card-file__info">
|
||
<i class="el-icon-document"></i>
|
||
<span class="selected-id-card-file__name">
|
||
{{ idCardFileList[0].name }}
|
||
</span>
|
||
</div>
|
||
<el-button type="text" @click="handleIdCardFileRemove">
|
||
移除
|
||
</el-button>
|
||
</div>
|
||
<div v-if="parsingIdCardFile" class="parse-tip">
|
||
正在解析身份证文件...
|
||
</div>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="时间跨度">
|
||
<el-date-picker
|
||
class="pull-bank-range-picker"
|
||
v-model="pullBankInfoForm.dateRange"
|
||
:disabled="isProjectTagging || isProjectArchived"
|
||
type="daterange"
|
||
value-format="yyyy-MM-dd"
|
||
:picker-options="pullBankInfoDatePickerOptions"
|
||
range-separator="至"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<span slot="footer">
|
||
<el-button @click="pullBankInfoDialogVisible = false">取消</el-button>
|
||
<el-button
|
||
type="primary"
|
||
:disabled="isProjectTagging || isProjectArchived"
|
||
:loading="pullBankInfoLoading"
|
||
@click="handleConfirmPullBankInfo"
|
||
>
|
||
确认拉取
|
||
</el-button>
|
||
</span>
|
||
</el-dialog>
|
||
|
||
<!-- 批量上传弹窗 -->
|
||
<el-dialog
|
||
title="批量上传流水文件"
|
||
:visible.sync="batchUploadDialogVisible"
|
||
:close-on-click-modal="false"
|
||
width="700px"
|
||
>
|
||
<el-upload
|
||
class="batch-upload-area"
|
||
drag
|
||
action="#"
|
||
multiple
|
||
:disabled="isProjectTagging || isProjectArchived"
|
||
:auto-upload="false"
|
||
:on-change="handleBatchFileChange"
|
||
:show-file-list="false"
|
||
:file-list="selectedFiles"
|
||
>
|
||
<i class="el-icon-upload"></i>
|
||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||
<div class="el-upload__tip" slot="tip">
|
||
支持 PDF、CSV、Excel 格式文件,最多100个文件,单个文件不超过50MB
|
||
</div>
|
||
</el-upload>
|
||
|
||
<div v-if="selectedFiles.length > 0" class="selected-files">
|
||
<div class="files-header">
|
||
<span>已选择 {{ selectedFiles.length }} 个文件</span>
|
||
</div>
|
||
<div class="files-list">
|
||
<div
|
||
v-for="(file, index) in selectedFiles"
|
||
:key="index"
|
||
class="file-item"
|
||
>
|
||
<i class="el-icon-document"></i>
|
||
<span class="file-name">{{ file.name }}</span>
|
||
<span class="file-size">{{ formatFileSize(file.size) }}</span>
|
||
<el-button
|
||
type="text"
|
||
icon="el-icon-close"
|
||
@click="handleRemoveFile(index)"
|
||
></el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<span slot="footer">
|
||
<el-button @click="batchUploadDialogVisible = false">取消</el-button>
|
||
<el-button
|
||
type="primary"
|
||
:disabled="isProjectTagging || isProjectArchived || selectedFiles.length === 0"
|
||
:loading="uploadLoading"
|
||
@click="handleBatchUpload"
|
||
>开始上传</el-button
|
||
>
|
||
</span>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import {
|
||
pullBankInfo,
|
||
parseIdCardFile,
|
||
batchUploadFiles,
|
||
getFileUploadList,
|
||
getFileUploadStatistics,
|
||
deleteFileUploadRecord,
|
||
} from "@/api/ccdiProjectUpload";
|
||
import { parseTime } from "@/utils/ruoyi";
|
||
import {
|
||
getUploadFileAction,
|
||
getUploadFileStatusText,
|
||
getUploadFileStatusType,
|
||
} from "./uploadFileActionRules";
|
||
|
||
export default {
|
||
name: "UploadData",
|
||
props: {
|
||
projectId: {
|
||
type: [String, Number],
|
||
default: null,
|
||
},
|
||
projectInfo: {
|
||
type: Object,
|
||
default: () => ({
|
||
projectName: "",
|
||
updateTime: "",
|
||
projectStatus: "0",
|
||
}),
|
||
},
|
||
},
|
||
data() {
|
||
return {
|
||
// 加载状态
|
||
loading: false,
|
||
// 当前选中的菜单
|
||
activeMenu: "upload",
|
||
// 当前菜单标题
|
||
currentMenuTitle: "上传数据",
|
||
pullBankInfoDialogVisible: false,
|
||
pullBankInfoLoading: false,
|
||
parsingIdCardFile: false,
|
||
idCardFileList: [],
|
||
pullBankInfoForm: {
|
||
idCardText: "",
|
||
dateRange: [],
|
||
},
|
||
// 侧边栏菜单项
|
||
menuItems: [
|
||
{ key: "upload", label: "上传数据", route: "upload" },
|
||
{ key: "config", label: "参数配置", route: "config" },
|
||
{ key: "overview", label: "结果总览", route: "overview" },
|
||
{ key: "special", label: "专项排查", route: "special" },
|
||
{ key: "detail", label: "流水明细查询", route: "detail" },
|
||
],
|
||
// === 批量上传相关 ===
|
||
batchUploadDialogVisible: false,
|
||
selectedFiles: [],
|
||
uploadLoading: false,
|
||
|
||
// === 统计数据 ===
|
||
statistics: {
|
||
uploading: 0,
|
||
parsing: 0,
|
||
parsed_success: 0,
|
||
parsed_failed: 0,
|
||
},
|
||
|
||
// === 文件列表相关 ===
|
||
fileUploadList: [],
|
||
listLoading: false,
|
||
queryParams: {
|
||
projectId: null,
|
||
pageNum: 1,
|
||
pageSize: 10,
|
||
},
|
||
total: 0,
|
||
|
||
// === 轮询相关 ===
|
||
pollingTimer: null,
|
||
pollingEnabled: false,
|
||
pollingInterval: 5000,
|
||
lastFileStatusSignature: "",
|
||
};
|
||
},
|
||
computed: {
|
||
pullBankInfoDatePickerOptions() {
|
||
return {
|
||
disabledDate: (time) => this.isPullBankInfoDateDisabled(time),
|
||
};
|
||
},
|
||
isProjectTagging() {
|
||
return String(this.projectInfo.projectStatus) === "3";
|
||
},
|
||
isProjectArchived() {
|
||
return String(this.projectInfo.projectStatus) === "2";
|
||
},
|
||
isReportDisabled() {
|
||
return ["0", "3"].includes(String(this.projectInfo.projectStatus));
|
||
},
|
||
},
|
||
created() {
|
||
this.updateActiveMenu();
|
||
},
|
||
mounted() {
|
||
// 加载统计数据和文件列表
|
||
this.loadStatistics();
|
||
this.loadFileList();
|
||
|
||
// 检查是否需要启动轮询
|
||
this.$nextTick(() => {
|
||
if (this.statistics.uploading > 0 || this.statistics.parsing > 0) {
|
||
this.startPolling();
|
||
}
|
||
});
|
||
},
|
||
beforeDestroy() {
|
||
this.stopPolling();
|
||
},
|
||
methods: {
|
||
/** 菜单点击 */
|
||
handleMenuClick(key, route) {
|
||
const menuItem = this.menuItems.find((m) => m.key === key);
|
||
if (menuItem) {
|
||
this.currentMenuTitle = menuItem.label;
|
||
}
|
||
if (key === "upload") {
|
||
this.activeMenu = key;
|
||
} else {
|
||
// 其他菜单项通知父组件跳转
|
||
this.$emit("menu-change", { key, route });
|
||
}
|
||
},
|
||
/** 更新当前选中菜单 */
|
||
updateActiveMenu() {
|
||
this.activeMenu = "upload";
|
||
this.currentMenuTitle = "上传数据";
|
||
},
|
||
handleViewReport() {
|
||
if (this.isReportDisabled) {
|
||
return;
|
||
}
|
||
this.$emit("menu-change", { key: "overview", route: "overview" });
|
||
},
|
||
handleGoCreditInfoPage() {
|
||
if (this.isProjectTagging || this.isProjectArchived) {
|
||
return;
|
||
}
|
||
this.$router.push("/maintain/creditInfo");
|
||
},
|
||
handleOpenBatchUploadDialog() {
|
||
if (this.isProjectTagging || this.isProjectArchived) {
|
||
return;
|
||
}
|
||
this.batchUploadDialogVisible = true;
|
||
this.selectedFiles = [];
|
||
},
|
||
openPullBankInfoDialog() {
|
||
this.pullBankInfoDialogVisible = true;
|
||
},
|
||
resetPullBankInfoForm() {
|
||
this.pullBankInfoForm = {
|
||
idCardText: "",
|
||
dateRange: this.buildDefaultPullBankInfoDateRange(),
|
||
};
|
||
this.idCardFileList = [];
|
||
this.parsingIdCardFile = false;
|
||
this.pullBankInfoLoading = false;
|
||
},
|
||
parseIdCardText(text) {
|
||
return Array.from(
|
||
new Set(
|
||
(text || "")
|
||
.split(/,+/)
|
||
.map((item) => item.trim())
|
||
.filter(Boolean)
|
||
)
|
||
);
|
||
},
|
||
mergeIdCards(currentText, parsedIdCards) {
|
||
const merged = [
|
||
...this.parseIdCardText(currentText),
|
||
...((parsedIdCards || [])
|
||
.map((item) => String(item || "").trim())
|
||
.filter(Boolean)),
|
||
];
|
||
return Array.from(new Set(merged)).join(", ");
|
||
},
|
||
async handleIdCardFileChange(file, fileList) {
|
||
if (this.isProjectTagging || this.isProjectArchived) {
|
||
return;
|
||
}
|
||
const latestFile = (fileList || []).slice(-1);
|
||
const currentFile = latestFile[0] || file;
|
||
const fileName = (currentFile && currentFile.name) || "";
|
||
const isExcel = /\.(xls|xlsx)$/i.test(fileName);
|
||
|
||
if (!isExcel) {
|
||
this.idCardFileList = [];
|
||
this.$message.error("仅支持上传 .xls 或 .xlsx 文件");
|
||
return;
|
||
}
|
||
|
||
if (!currentFile || !currentFile.raw) {
|
||
this.idCardFileList = [];
|
||
this.$message.error("未获取到有效文件");
|
||
return;
|
||
}
|
||
|
||
this.idCardFileList = latestFile;
|
||
this.parsingIdCardFile = true;
|
||
|
||
try {
|
||
const res = await parseIdCardFile(currentFile.raw);
|
||
const parsedIdCards =
|
||
(res && res.data && Array.isArray(res.data.idCards) && res.data.idCards) ||
|
||
[];
|
||
this.pullBankInfoForm.idCardText = this.mergeIdCards(
|
||
this.pullBankInfoForm.idCardText,
|
||
parsedIdCards
|
||
);
|
||
this.$message.success(
|
||
`身份证文件解析成功,共 ${parsedIdCards.length} 条有效身份证`
|
||
);
|
||
} catch (error) {
|
||
this.idCardFileList = [];
|
||
this.$message.error(
|
||
"身份证文件解析失败:" +
|
||
((error && error.message) || "未知错误")
|
||
);
|
||
} finally {
|
||
this.parsingIdCardFile = false;
|
||
}
|
||
},
|
||
handleIdCardFileRemove() {
|
||
this.idCardFileList = [];
|
||
this.parsingIdCardFile = false;
|
||
},
|
||
getPullBankInfoTodayStart() {
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
return today;
|
||
},
|
||
getPullBankInfoMinSelectableDate() {
|
||
return new Date(2025, 0, 1);
|
||
},
|
||
getPullBankInfoMaxSelectableDate() {
|
||
const yesterday = this.getPullBankInfoTodayStart();
|
||
yesterday.setDate(yesterday.getDate() - 1);
|
||
return yesterday;
|
||
},
|
||
formatPullBankInfoDate(date) {
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||
const day = String(date.getDate()).padStart(2, "0");
|
||
return `${year}-${month}-${day}`;
|
||
},
|
||
buildDefaultPullBankInfoDateRange() {
|
||
const minSelectableDate = this.getPullBankInfoMinSelectableDate();
|
||
const maxSelectableDate = this.getPullBankInfoMaxSelectableDate();
|
||
const defaultStartDate = new Date(maxSelectableDate.getTime());
|
||
defaultStartDate.setFullYear(defaultStartDate.getFullYear() - 1);
|
||
if (defaultStartDate.getTime() < minSelectableDate.getTime()) {
|
||
return [
|
||
this.formatPullBankInfoDate(minSelectableDate),
|
||
this.formatPullBankInfoDate(maxSelectableDate),
|
||
];
|
||
}
|
||
return [
|
||
this.formatPullBankInfoDate(defaultStartDate),
|
||
this.formatPullBankInfoDate(maxSelectableDate),
|
||
];
|
||
},
|
||
parsePullBankInfoDate(dateValue) {
|
||
if (!dateValue) return null;
|
||
const [year, month, day] = String(dateValue)
|
||
.split("-")
|
||
.map((item) => Number(item));
|
||
if (![year, month, day].every(Number.isFinite)) {
|
||
return null;
|
||
}
|
||
return new Date(year, month - 1, day);
|
||
},
|
||
isPullBankInfoDateDisabled(time) {
|
||
const minSelectableDate = this.getPullBankInfoMinSelectableDate();
|
||
const todayStart = this.getPullBankInfoTodayStart();
|
||
return (
|
||
time.getTime() < minSelectableDate.getTime() ||
|
||
time.getTime() >= todayStart.getTime()
|
||
);
|
||
},
|
||
hasInvalidPullBankInfoDateRange(dateRange) {
|
||
const minSelectableDate = this.getPullBankInfoMinSelectableDate();
|
||
const maxSelectableDate = this.getPullBankInfoMaxSelectableDate();
|
||
return (dateRange || []).some((dateValue) => {
|
||
const date = this.parsePullBankInfoDate(dateValue);
|
||
return (
|
||
!date ||
|
||
date.getTime() < minSelectableDate.getTime() ||
|
||
date.getTime() > maxSelectableDate.getTime()
|
||
);
|
||
});
|
||
},
|
||
buildFinalIdCardList() {
|
||
return this.parseIdCardText(this.pullBankInfoForm.idCardText);
|
||
},
|
||
async handleConfirmPullBankInfo() {
|
||
if (this.isProjectTagging || this.isProjectArchived) {
|
||
this.$message.warning(
|
||
this.isProjectArchived
|
||
? "项目已归档,暂不可上传或拉取数据"
|
||
: "项目正在进行银行流水打标,暂不可上传或拉取数据"
|
||
);
|
||
return;
|
||
}
|
||
const idCards = this.buildFinalIdCardList();
|
||
const [startDate, endDate] = this.pullBankInfoForm.dateRange || [];
|
||
|
||
if (idCards.length === 0) {
|
||
this.$message.warning("请至少输入一个身份证号");
|
||
return;
|
||
}
|
||
|
||
if (!startDate || !endDate) {
|
||
this.$message.warning("请选择完整的时间跨度");
|
||
return;
|
||
}
|
||
|
||
if (this.hasInvalidPullBankInfoDateRange([startDate, endDate])) {
|
||
this.$message.warning("时间跨度仅支持 2025-01-01 至昨天");
|
||
return;
|
||
}
|
||
|
||
this.pullBankInfoLoading = true;
|
||
|
||
try {
|
||
const payload = {
|
||
projectId: this.projectId,
|
||
idCards,
|
||
startDate,
|
||
endDate,
|
||
};
|
||
const res = await pullBankInfo(payload);
|
||
|
||
this.pullBankInfoDialogVisible = false;
|
||
this.resetPullBankInfoForm();
|
||
this.$message.success((res && res.msg) || "拉取任务已提交");
|
||
|
||
await Promise.all([this.loadStatistics(), this.loadFileList()]);
|
||
this.lastFileStatusSignature = this.buildFileStatusSignature();
|
||
this.$emit("refresh-project");
|
||
|
||
const hasPollingRecords =
|
||
this.statistics.uploading > 0 ||
|
||
this.statistics.parsing > 0 ||
|
||
this.fileUploadList.some((item) =>
|
||
["uploading", "parsing"].includes(item.fileStatus)
|
||
);
|
||
|
||
if (hasPollingRecords) {
|
||
this.startPolling();
|
||
}
|
||
} catch (error) {
|
||
this.pullBankInfoLoading = false;
|
||
this.$message.error(
|
||
"拉取本行信息失败:" + ((error && error.message) || "未知错误")
|
||
);
|
||
}
|
||
},
|
||
/** 拉取本行信息 */
|
||
handleFetchBankInfo() {
|
||
if (this.isProjectTagging || this.isProjectArchived) {
|
||
this.$message.warning(
|
||
this.isProjectArchived
|
||
? "项目已归档,暂不可上传或拉取数据"
|
||
: "项目正在进行银行流水打标,暂不可上传或拉取数据"
|
||
);
|
||
return;
|
||
}
|
||
this.resetPullBankInfoForm();
|
||
this.openPullBankInfoDialog();
|
||
},
|
||
/** 格式化更新时间 */
|
||
formatUpdateTime(time) {
|
||
if (!time) return "-";
|
||
const date = new Date(time);
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||
const day = String(date.getDate()).padStart(2, "0");
|
||
const hours = String(date.getHours()).padStart(2, "0");
|
||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||
},
|
||
/** 获取状态样式类 */
|
||
getStatusClass() {
|
||
const status = String(this.projectInfo.projectStatus);
|
||
const statusMap = {
|
||
0: "processing",
|
||
1: "success",
|
||
2: "archived",
|
||
3: "tagging",
|
||
};
|
||
return statusMap[status] || "processing";
|
||
},
|
||
/** 获取状态标签 */
|
||
getStatusLabel() {
|
||
const status = String(this.projectInfo.projectStatus);
|
||
const statusMap = {
|
||
0: "进行中",
|
||
1: "已完成",
|
||
2: "已归档",
|
||
3: "打标中",
|
||
};
|
||
return statusMap[status] || "未知";
|
||
},
|
||
|
||
// === 批量上传相关方法 ===
|
||
|
||
/** 批量上传的文件选择变化 */
|
||
handleBatchFileChange(file, fileList) {
|
||
if (fileList.length > 100) {
|
||
this.$message.warning("最多上传100个文件");
|
||
fileList = fileList.slice(0, 100);
|
||
}
|
||
|
||
const validTypes = ['.pdf', '.csv', '.xlsx', '.xls'];
|
||
const invalidFiles = fileList.filter((f) => {
|
||
const ext = f.name.substring(f.name.lastIndexOf(".")).toLowerCase();
|
||
return !validTypes.includes(ext);
|
||
});
|
||
|
||
if (invalidFiles.length > 0) {
|
||
this.$message.error("仅支持 PDF、CSV、Excel 格式文件");
|
||
return;
|
||
}
|
||
|
||
const oversizedFiles = fileList.filter((f) => f.size > 50 * 1024 * 1024);
|
||
if (oversizedFiles.length > 0) {
|
||
this.$message.error("单个文件不能超过50MB");
|
||
return;
|
||
}
|
||
|
||
this.selectedFiles = fileList;
|
||
},
|
||
|
||
/** 删除已选文件 */
|
||
handleRemoveFile(index) {
|
||
this.selectedFiles.splice(index, 1);
|
||
},
|
||
|
||
/** 开始批量上传 */
|
||
async handleBatchUpload() {
|
||
if (this.isProjectTagging || this.isProjectArchived) {
|
||
this.$message.warning(
|
||
this.isProjectArchived
|
||
? "项目已归档,暂不可上传或拉取数据"
|
||
: "项目正在进行银行流水打标,暂不可上传或拉取数据"
|
||
);
|
||
return;
|
||
}
|
||
if (this.selectedFiles.length === 0) {
|
||
this.$message.warning("请选择要上传的文件");
|
||
return;
|
||
}
|
||
|
||
this.uploadLoading = true;
|
||
|
||
try {
|
||
const res = await batchUploadFiles(
|
||
this.projectId,
|
||
this.selectedFiles.map((f) => f.raw)
|
||
);
|
||
|
||
this.uploadLoading = false;
|
||
this.batchUploadDialogVisible = false;
|
||
|
||
this.$message.success("上传任务已提交,请查看处理进度");
|
||
|
||
// 刷新数据并启动轮询
|
||
await Promise.all([this.loadStatistics(), this.loadFileList()]);
|
||
this.lastFileStatusSignature = this.buildFileStatusSignature();
|
||
this.$emit("refresh-project");
|
||
|
||
this.startPolling();
|
||
} catch (error) {
|
||
this.uploadLoading = false;
|
||
this.$message.error("上传失败:" + (error.msg || "未知错误"));
|
||
}
|
||
},
|
||
|
||
// === 统计和列表相关方法 ===
|
||
|
||
/** 加载统计数据 */
|
||
async loadStatistics() {
|
||
try {
|
||
const res = await getFileUploadStatistics(this.projectId);
|
||
this.statistics = res.data || {
|
||
uploading: 0,
|
||
parsing: 0,
|
||
parsed_success: 0,
|
||
parsed_failed: 0,
|
||
};
|
||
} catch (error) {
|
||
console.error("加载统计数据失败:", error);
|
||
}
|
||
},
|
||
|
||
/** 加载文件列表 */
|
||
async loadFileList() {
|
||
this.listLoading = true;
|
||
|
||
try {
|
||
const params = {
|
||
projectId: this.projectId,
|
||
pageNum: this.queryParams.pageNum,
|
||
pageSize: this.queryParams.pageSize,
|
||
};
|
||
|
||
const res = await getFileUploadList(params);
|
||
this.fileUploadList = res.rows || [];
|
||
this.total = res.total || 0;
|
||
if (!this.lastFileStatusSignature) {
|
||
this.lastFileStatusSignature = this.buildFileStatusSignature();
|
||
}
|
||
} catch (error) {
|
||
this.$message.error("加载文件列表失败");
|
||
console.error(error);
|
||
} finally {
|
||
this.listLoading = false;
|
||
}
|
||
},
|
||
|
||
// === 轮询相关方法 ===
|
||
|
||
/** 启动轮询 */
|
||
startPolling() {
|
||
if (this.pollingEnabled) return;
|
||
this.pollingEnabled = true;
|
||
|
||
const poll = () => {
|
||
if (!this.pollingEnabled) return;
|
||
|
||
Promise.all([this.loadStatistics(), this.loadFileList()])
|
||
.then(() => {
|
||
const currentSignature = this.buildFileStatusSignature();
|
||
if (currentSignature !== this.lastFileStatusSignature) {
|
||
this.lastFileStatusSignature = currentSignature;
|
||
this.$emit("refresh-project");
|
||
}
|
||
if (
|
||
this.statistics.uploading === 0 &&
|
||
this.statistics.parsing === 0
|
||
) {
|
||
this.stopPolling();
|
||
return;
|
||
}
|
||
this.pollingTimer = setTimeout(poll, this.pollingInterval);
|
||
})
|
||
.catch((error) => {
|
||
console.error("轮询失败:", error);
|
||
this.pollingTimer = setTimeout(poll, this.pollingInterval);
|
||
});
|
||
};
|
||
|
||
poll();
|
||
},
|
||
|
||
/** 停止轮询 */
|
||
stopPolling() {
|
||
this.pollingEnabled = false;
|
||
if (this.pollingTimer) {
|
||
clearTimeout(this.pollingTimer);
|
||
this.pollingTimer = null;
|
||
}
|
||
},
|
||
|
||
/** 手动刷新 */
|
||
async handleManualRefresh() {
|
||
await Promise.all([this.loadStatistics(), this.loadFileList()]);
|
||
this.lastFileStatusSignature = this.buildFileStatusSignature();
|
||
this.$emit("refresh-project");
|
||
|
||
this.$message.success("刷新成功");
|
||
|
||
if (this.statistics.uploading > 0 || this.statistics.parsing > 0) {
|
||
this.startPolling();
|
||
}
|
||
},
|
||
|
||
// === 辅助方法 ===
|
||
|
||
/** 分页变化 */
|
||
handlePageChange(pageNum) {
|
||
this.queryParams.pageNum = pageNum;
|
||
this.loadFileList();
|
||
},
|
||
buildFileStatusSignature() {
|
||
return (this.fileUploadList || [])
|
||
.map((item) => `${item.id || ""}:${item.fileStatus || ""}`)
|
||
.join("|");
|
||
},
|
||
|
||
getRowAction(row) {
|
||
return getUploadFileAction(row);
|
||
},
|
||
|
||
handleRowAction(row) {
|
||
const action = this.getRowAction(row);
|
||
if (!action) {
|
||
return;
|
||
}
|
||
|
||
if (action.key === "viewError") {
|
||
this.handleViewError(row);
|
||
return;
|
||
}
|
||
|
||
if (action.key === "delete") {
|
||
this.handleDeleteFile(row);
|
||
}
|
||
},
|
||
|
||
handleViewError(row) {
|
||
this.$alert(row.errorMessage || "未知错误", "错误信息", {
|
||
confirmButtonText: "确定",
|
||
type: "error",
|
||
});
|
||
},
|
||
|
||
async handleDeleteFile(row) {
|
||
try {
|
||
await this.$confirm(
|
||
"删除该文件后将同步删除流水分析平台中的文件,并清除本系统中该文件对应的所有银行流水数据,项目内流水也将重新打标,是否继续?",
|
||
"提示",
|
||
{
|
||
confirmButtonText: "确定",
|
||
cancelButtonText: "取消",
|
||
type: "warning",
|
||
}
|
||
);
|
||
|
||
await deleteFileUploadRecord(row.id);
|
||
this.$message.success("删除成功,已开始项目重新打标");
|
||
await Promise.all([this.loadStatistics(), this.loadFileList()]);
|
||
|
||
if (this.hasActivePollingRecords()) {
|
||
this.startPolling();
|
||
} else {
|
||
this.stopPolling();
|
||
}
|
||
} catch (error) {
|
||
if (error === "cancel" || error === "close") {
|
||
return;
|
||
}
|
||
|
||
this.$message.error("删除失败:" + ((error && error.message) || "未知错误"));
|
||
}
|
||
},
|
||
|
||
hasActivePollingRecords() {
|
||
return (
|
||
this.statistics.uploading > 0 ||
|
||
this.statistics.parsing > 0 ||
|
||
this.fileUploadList.some((item) =>
|
||
["uploading", "parsing"].includes(item.fileStatus)
|
||
)
|
||
);
|
||
},
|
||
|
||
/** 状态文本映射 */
|
||
getStatusText(status) {
|
||
return getUploadFileStatusText(status);
|
||
},
|
||
|
||
/** 状态标签类型映射 */
|
||
getStatusType(status) {
|
||
return getUploadFileStatusType(status);
|
||
},
|
||
|
||
/** 格式化文件大小 */
|
||
formatFileSize(bytes) {
|
||
if (bytes === 0) return "0 B";
|
||
const k = 1024;
|
||
const sizes = ["B", "KB", "MB", "GB"];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||
},
|
||
/** 格式化上传时间 */
|
||
formatUploadTime(time) {
|
||
const formatted = parseTime(time, "{y}-{m}-{d} {h}:{i}:{s}");
|
||
return formatted || "-";
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.upload-data-container {
|
||
padding: 16px;
|
||
background: transparent;
|
||
min-height: 100%;
|
||
}
|
||
|
||
// 侧边栏
|
||
.sidebar {
|
||
background: #ffffff;
|
||
border-radius: 4px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||
padding: 16px;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.sidebar-header {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 12px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
|
||
.sidebar-menu {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0 0 auto 0;
|
||
flex: 1;
|
||
|
||
.menu-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10px 12px;
|
||
margin-bottom: 4px;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
color: #606266;
|
||
transition: all 0.3s;
|
||
position: relative;
|
||
|
||
&:hover {
|
||
background: #f5f7fa;
|
||
color: #303133;
|
||
}
|
||
|
||
&.active {
|
||
background: #1890ff;
|
||
color: #ffffff;
|
||
|
||
.menu-dot {
|
||
display: block;
|
||
}
|
||
}
|
||
|
||
.menu-dot {
|
||
display: none;
|
||
width: 4px;
|
||
height: 4px;
|
||
background: #ffffff;
|
||
border-radius: 50%;
|
||
margin-right: 8px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.sidebar-footer {
|
||
border-top: 1px solid #ebeef5;
|
||
padding-top: 16px;
|
||
margin-top: 16px;
|
||
|
||
.status {
|
||
font-size: 13px;
|
||
color: #606266;
|
||
margin-bottom: 8px;
|
||
|
||
.status-processing {
|
||
color: #1890ff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.status-success {
|
||
color: #52c41a;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.status-archived {
|
||
color: #909399;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.update-time {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 主内容区
|
||
.main-content {
|
||
.tagging-lock-tip {
|
||
margin-bottom: 16px;
|
||
padding: 10px 14px;
|
||
color: #a56a2a;
|
||
background: #fbf3ea;
|
||
border: 1px solid #eddcc8;
|
||
border-radius: 10px;
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65);
|
||
}
|
||
}
|
||
|
||
// 文件列表区域
|
||
.file-list-section {
|
||
background: #fff;
|
||
border: 1px solid var(--ccdi-border);
|
||
border-radius: 12px;
|
||
padding: 20px 24px 24px;
|
||
box-shadow: var(--ccdi-shadow);
|
||
|
||
.list-toolbar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
padding-bottom: 12px;
|
||
border-bottom: 1px solid var(--ccdi-line);
|
||
|
||
.toolbar-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
|
||
.el-button {
|
||
min-width: 104px;
|
||
}
|
||
}
|
||
}
|
||
|
||
::v-deep .el-table {
|
||
border: 1px solid var(--ccdi-border);
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
::v-deep .el-table th {
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
// 上传弹窗样式
|
||
::v-deep .el-dialog__wrapper {
|
||
.upload-area,
|
||
.batch-upload-area {
|
||
width: 100%;
|
||
}
|
||
|
||
.upload-area .el-upload,
|
||
.batch-upload-area .el-upload {
|
||
width: 100%;
|
||
}
|
||
|
||
.upload-area .el-upload-dragger,
|
||
.batch-upload-area .el-upload-dragger {
|
||
width: 100%;
|
||
height: 200px;
|
||
}
|
||
|
||
.el-upload__tip {
|
||
margin-top: 8px;
|
||
color: var(--ccdi-text-muted);
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
.pull-bank-info-form {
|
||
.pull-bank-field-tip {
|
||
margin-top: 8px;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
line-height: 1.5;
|
||
}
|
||
}
|
||
|
||
.pull-bank-file-panel {
|
||
padding: 16px;
|
||
border: 1px solid var(--ccdi-border);
|
||
border-radius: 8px;
|
||
background: #f8fbfe;
|
||
|
||
.pull-bank-file-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.pull-bank-file-tip {
|
||
font-size: 12px;
|
||
color: var(--ccdi-text-secondary);
|
||
line-height: 1.5;
|
||
}
|
||
}
|
||
|
||
.pull-bank-file-upload {
|
||
display: inline-flex;
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.selected-id-card-file {
|
||
margin-top: 12px;
|
||
padding: 10px 12px;
|
||
border-radius: 6px;
|
||
background: #ffffff;
|
||
border: 1px solid var(--ccdi-border);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
|
||
.selected-id-card-file__info {
|
||
min-width: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: var(--ccdi-text-primary);
|
||
}
|
||
|
||
.selected-id-card-file__name {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.el-button {
|
||
flex-shrink: 0;
|
||
padding: 0;
|
||
}
|
||
}
|
||
|
||
.pull-bank-range-picker {
|
||
width: 100%;
|
||
}
|
||
|
||
// 批量上传弹窗样式
|
||
.batch-upload-area {
|
||
width: 100%;
|
||
|
||
::v-deep .el-upload {
|
||
width: 100%;
|
||
|
||
.el-upload-dragger {
|
||
width: 100%;
|
||
height: 180px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.selected-files {
|
||
margin-top: 16px;
|
||
border: 1px solid var(--ccdi-border);
|
||
border-radius: 4px;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
|
||
.files-header {
|
||
padding: 12px 16px;
|
||
background: #f7fafd;
|
||
border-bottom: 1px solid var(--ccdi-line);
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: var(--ccdi-text-secondary);
|
||
}
|
||
|
||
.files-list {
|
||
padding: 8px;
|
||
|
||
.file-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 8px 12px;
|
||
border-radius: 4px;
|
||
transition: background 0.3s;
|
||
|
||
&:hover {
|
||
background: #f7fafd;
|
||
}
|
||
|
||
i {
|
||
font-size: 18px;
|
||
color: var(--ccdi-primary);
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.file-name {
|
||
flex: 1;
|
||
font-size: 14px;
|
||
color: var(--ccdi-text-primary);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.file-size {
|
||
font-size: 12px;
|
||
color: var(--ccdi-text-muted);
|
||
margin: 0 12px;
|
||
}
|
||
|
||
.el-button {
|
||
padding: 4px;
|
||
color: var(--ccdi-text-muted);
|
||
|
||
&:hover {
|
||
color: #b55252;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.parse-tip {
|
||
margin-top: 8px;
|
||
font-size: 12px;
|
||
color: var(--ccdi-text-muted);
|
||
}
|
||
|
||
// 响应式
|
||
@media (max-width: 1200px) {
|
||
.file-list-section .list-toolbar .toolbar-actions {
|
||
gap: 10px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.upload-data-container {
|
||
padding: 8px;
|
||
}
|
||
|
||
.sidebar {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.file-list-section .list-toolbar {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
|
||
.toolbar-actions {
|
||
width: 100%;
|
||
justify-content: flex-start;
|
||
}
|
||
}
|
||
|
||
.pull-bank-file-panel {
|
||
padding: 12px;
|
||
|
||
.pull-bank-file-actions {
|
||
align-items: stretch;
|
||
gap: 10px;
|
||
}
|
||
}
|
||
|
||
.pull-bank-file-upload {
|
||
width: 100%;
|
||
}
|
||
|
||
.selected-id-card-file {
|
||
align-items: flex-start;
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
</style>
|