Files
ccdi/ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue
2026-03-04 15:19:55 +08:00

937 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="upload-data-container">
<!-- 主内容区 -->
<div class="main-content">
<!-- 页面头部 -->
<div class="content-header">
<h2 class="content-title">{{ currentMenuTitle }}</h2>
<div class="header-actions">
<el-button
size="small"
type="primary"
icon="el-icon-document"
@click="handleGenerateReport"
>
生成报告
</el-button>
<el-button
size="small"
icon="el-icon-download"
@click="handleFetchBankInfo"
>
拉取本行信息
</el-button>
</div>
</div>
<!-- 上传模块 -->
<div class="upload-section">
<div class="upload-cards">
<div v-for="card in uploadCards" :key="card.key" class="upload-card">
<div class="card-icon">
<i :class="card.icon"></i>
</div>
<div class="card-title">{{ card.title }}</div>
<div class="card-desc">{{ card.desc }}</div>
<el-button
size="small"
:type="card.uploaded ? 'primary' : ''"
:icon="card.uploaded ? 'el-icon-view' : 'el-icon-upload2'"
:plain="!card.uploaded"
@click="handleUploadClick(card.key)"
>
{{ card.btnText }}
</el-button>
</div>
</div>
</div>
<!-- 数据质量检查
<div class="quality-check-section">
<div class="section-header">
<i class="el-icon-warning-outline warning-icon"></i>
<span>检查结果</span>
</div>
<div class="metrics">
<div v-for="metric in metrics" :key="metric.key" class="metric-card">
<div class="metric-title">{{ metric.title }}</div>
<div class="metric-value" :class="`value-${metric.level}`">
{{ metric.value }}
</div>
<div class="progress-ring-container">
<svg class="progress-ring" viewBox="0 0 32 32">
<circle
class="progress-ring-bg"
cx="16"
cy="16"
r="14"
fill="none"
stroke="#f0f0f0"
stroke-width="4"
/>
<circle
class="progress-ring-progress"
:stroke="getProgressColor(metric.level)"
cx="16"
cy="16"
r="14"
fill="none"
stroke-width="4"
:stroke-dasharray="circumference"
:stroke-dashoffset="getProgressOffset(metric.value)"
stroke-linecap="round"
/>
</svg>
</div>
</div>
</div>
</div> -->
</div>
<!-- 上传弹窗 -->
<el-dialog
v-if="showUploadDialog"
:title="uploadDialogTitle"
:visible.sync="showUploadDialog"
:close-on-click-modal="false"
width="500px"
>
<el-upload
class="upload-area"
drag
action="#"
:auto-upload="false"
:on-change="handleFileChange"
:file-list="fileList"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">
支持 {{ uploadFileTypes }} 格式文件
</div>
</el-upload>
<span slot="footer">
<el-button @click="showUploadDialog = false">取消</el-button>
<el-button
type="primary"
@click="handleConfirmUpload"
:loading="uploading"
>确定</el-button
>
</span>
</el-dialog>
<!-- 名单选择弹窗 -->
<el-dialog
:title="'名单库选择'"
:visible.sync="showNameListDialog"
width="600px"
>
<el-form :model="nameListForm" label-width="100px">
<el-form-item label="名单类型">
<el-select v-model="nameListForm.type" placeholder="请选择名单类型">
<el-option label="黑名单" value="blacklist"></el-option>
<el-option label="灰名单" value="graylist"></el-option>
<el-option label="白名单" value="whitelist"></el-option>
</el-select>
</el-form-item>
<el-form-item label="名单来源">
<el-select v-model="nameListForm.source" placeholder="请选择名单来源">
<el-option label="中台管理系统" value="platform"></el-option>
<el-option label="本地上传" value="local"></el-option>
</el-select>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="showNameListDialog = false">取消</el-button>
<el-button type="primary" @click="handleConfirmNameList"
>确定</el-button
>
</span>
</el-dialog>
</div>
</template>
<script>
import {
getImportStatus,
getNameListOptions,
getUploadStatus,
pullBankInfo,
updateNameListSelection,
uploadFile,
} from "@/api/ccdiProjectUpload";
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: "上传数据",
// 圆环周长
circumference: 2 * Math.PI * 14,
// 上传弹窗
showUploadDialog: false,
uploadDialogTitle: "",
uploadFileType: "",
uploadFileTypes: "",
fileList: [],
uploading: false,
// 名单选择弹窗
showNameListDialog: false,
nameListForm: {
type: "",
source: "platform",
},
// 上传状态列表
uploadStatusList: [],
// 名单库选项列表
nameListOptions: [],
// 侧边栏菜单项
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" },
],
// 上传卡片
uploadCards: [
{
key: "transaction",
title: "流水导入",
desc: "支持 Excel、PDF 格式文件上传",
icon: "el-icon-document",
btnText: "上传流水",
uploaded: false,
},
{
key: "credit",
title: "征信导入",
desc: "支持 HTML 格式征信数据解析",
icon: "el-icon-s-data",
btnText: "上传征信",
uploaded: false,
},
{
key: "namelist",
title: "名单库选择",
desc: "选择中台管理系统的名单",
icon: "el-icon-s-order",
btnText: "选择名单",
uploaded: false,
},
],
// 质量指标
metrics: [
{
key: "completeness",
title: "数据完整性",
value: "98.5%",
level: "success",
},
{
key: "consistency",
title: "格式一致性",
value: "95.2%",
level: "info",
},
{
key: "continuity",
title: "余额连续性",
value: "92.8%",
level: "info",
},
],
};
},
created() {
// 加载初始数据
// this.loadInitialData();
// 监听路由变化更新选中菜单
this.updateActiveMenu();
},
mounted() {
// 组件挂载后监听项目ID变化
this.$watch("projectId", this.loadInitialData);
},
methods: {
/** 加载初始数据 */
async loadInitialData() {
if (!this.projectId) return;
try {
this.loading = true;
// 并行加载上传状态和名单库选项
const [uploadStatusRes, nameListRes] = await Promise.all([
getUploadStatus(this.projectId),
getNameListOptions(),
]);
this.uploadStatusList = uploadStatusRes.data || [];
this.nameListOptions = nameListRes.data || [];
// 更新上传卡片状态
this.updateUploadCards();
// 模拟更新质量指标实际应从API获取
this.updateQualityMetrics();
} catch (error) {
console.error("加载初始数据失败:", error);
this.$message.error("加载数据失败");
} finally {
this.loading = false;
}
},
/** 更新上传卡片状态 */
updateUploadCards() {
const statusMap = {};
this.uploadStatusList.forEach((item) => {
statusMap[item.uploadType] = item;
});
this.uploadCards.forEach((card) => {
const status = statusMap[card.key.toUpperCase()];
if (status) {
card.uploaded = status.uploaded;
card.btnText = status.uploaded
? "已上传" + card.title.replace("导入", "").replace("上传", "")
: card.btnText;
}
});
},
/** 更新质量指标 */
updateQualityMetrics() {
// 模拟更新质量指标
this.metrics.forEach((metric) => {
if (metric.key === "completeness") {
metric.value = "98.5%";
metric.level = "success";
} else if (metric.key === "consistency") {
metric.value = "95.2%";
metric.level = "info";
} else if (metric.key === "continuity") {
metric.value = "92.8%";
metric.level = "info";
}
});
},
/** 菜单点击 */
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 = "上传数据";
},
/** 上传卡片点击 */
handleUploadClick(key) {
const card = this.uploadCards.find((c) => c.key === key);
if (!card) return;
if (key === "namelist") {
this.showNameListDialog = true;
} else {
this.uploadFileType = key;
this.uploadDialogTitle = `上传${card.title}`;
this.uploadFileTypes = card.desc.replace(/.*支持|上传/g, "").trim();
this.showUploadDialog = true;
}
},
/** 文件选择变化 */
handleFileChange(file, fileList) {
this.fileList = fileList.slice(-1); // 只保留最后一个文件
},
/** 确认上传 */
async handleConfirmUpload() {
if (this.fileList.length === 0) {
this.$message.warning("请选择要上传的文件");
return;
}
this.uploading = true;
try {
// 调用上传API
const res = await uploadFile(
this.projectId,
this.uploadFileType.toUpperCase(),
this.fileList[0].raw
);
this.uploading = false;
this.showUploadDialog = false;
this.$message.success("文件上传成功,正在处理中...");
this.$emit("data-uploaded", { type: this.uploadFileType });
// 刷新上传状态
await this.loadUploadStatus();
// 开始轮询任务状态
this.startPolling(res.data);
} catch (error) {
this.uploading = false;
this.$message.error("上传失败:" + (error.msg || "未知错误"));
} finally {
this.fileList = [];
}
},
/** 轮询任务状态 */
startPolling(taskId) {
const maxAttempts = 20; // 最多轮询20次约10分钟
let attempts = 0;
const poll = async () => {
if (attempts >= maxAttempts) {
this.$message.warning("文件处理超时,请稍后查看");
return;
}
try {
const res = await getImportStatus(taskId);
const status = res.data;
if (!status) {
attempts++;
setTimeout(poll, 30000); // 30秒后再次查询
return;
}
if (status.uploadStatus === "SUCCESS") {
// 处理完成,刷新状态
await this.loadUploadStatus();
this.$message.success("文件处理完成");
return;
} else if (status.uploadStatus === "FAILED") {
// 处理失败
await this.loadUploadStatus();
this.$message.error(
"文件处理失败:" + (status.errorMessage || "未知错误")
);
return;
}
// 继续处理中
attempts++;
setTimeout(poll, 30000);
} catch (error) {
console.error("轮询任务状态失败:", error);
attempts++;
setTimeout(poll, 30000);
}
};
poll();
},
/** 加载上传状态 */
async loadUploadStatus() {
try {
const res = await getUploadStatus(this.projectId);
this.uploadStatusList = res.data || [];
this.updateUploadCards();
} catch (error) {
console.error("加载上传状态失败:", error);
}
},
/** 确认选择名单 */
async handleConfirmNameList() {
if (!this.nameListForm.type || !this.nameListForm.source) {
this.$message.warning("请完善名单选择信息");
return;
}
try {
// 调用更新名单API
await updateNameListSelection(this.projectId, {
nameLists: [
{
type: this.nameListForm.type,
source: this.nameListForm.source,
},
],
});
const card = this.uploadCards.find((c) => c.key === "namelist");
if (card) {
card.uploaded = true;
card.btnText = "已选择名单";
}
this.showNameListDialog = false;
this.$message.success("名单选择成功");
this.$emit("name-selected", this.nameListForm);
} catch (error) {
this.$message.error("名单选择失败:" + (error.msg || "未知错误"));
}
},
/** 生成报告 */
async handleGenerateReport() {
this.$confirm("确认生成报告吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
const loading = this.$loading({
lock: true,
text: "正在生成报告...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)",
});
// await generateReport(this.projectId);
loading.close();
this.$message.success("报告生成成功");
this.$emit("generate-report");
} catch (error) {
this.$message.error("生成报告失败:" + (error.msg || "未知错误"));
}
})
.catch(() => {});
},
/** 拉取本行信息 */
async handleFetchBankInfo() {
this.$confirm("确认拉取本行信息吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
const loading = this.$loading({
lock: true,
text: "正在拉取本行信息...",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)",
});
await pullBankInfo(this.projectId);
loading.close();
this.$message.success("本行信息拉取成功");
this.$emit("fetch-bank-info");
// 刷新质量指标
this.updateQualityMetrics();
} catch (error) {
this.$message.error(
"拉取本行信息失败:" + (error.msg || "未知错误")
);
}
})
.catch(() => {});
},
/** 获取进度条偏移 */
getProgressOffset(value) {
const percentage = parseFloat(value);
return this.circumference * (1 - percentage / 100);
},
/** 获取进度条颜色 */
getProgressColor(level) {
const colorMap = {
success: "#52c41a",
info: "#1890ff",
warning: "#fa8c16",
danger: "#f5222d",
};
return colorMap[level] || "#1890ff";
},
/** 格式化更新时间 */
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",
};
return statusMap[status] || "processing";
},
/** 获取状态标签 */
getStatusLabel() {
const status = String(this.projectInfo.projectStatus);
const statusMap = {
0: "进行中",
1: "已完成",
2: "已归档",
};
return statusMap[status] || "未知";
},
},
};
</script>
<style lang="scss" scoped>
.upload-data-container {
padding: 16px;
background: #fff;
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 {
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.content-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #303133;
}
.header-actions {
display: flex;
gap: 12px;
}
}
// 上传模块
.upload-section {
background: #ffffff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
padding: 20px;
margin-bottom: 16px;
.upload-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
.upload-card {
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 20px 16px;
text-align: center;
transition: all 0.3s;
background-color: #fff;
&:hover {
border-color: #1890ff;
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15);
}
.card-icon {
font-size: 32px;
color: #1890ff;
margin-bottom: 12px;
i {
font-size: 32px;
}
}
.card-title {
font-size: 16px;
font-weight: 500;
color: #303133;
margin-bottom: 8px;
}
.card-desc {
font-size: 13px;
color: #909399;
margin-bottom: 16px;
min-height: 36px;
line-height: 1.4;
}
.el-upload {
width: 100%;
}
}
}
}
// 数据质量检查
.quality-check-section {
background: #ffffff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
padding: 20px;
.section-header {
display: flex;
align-items: center;
margin-bottom: 20px;
.warning-icon {
font-size: 18px;
color: #fa8c16;
margin-right: 8px;
}
span {
font-size: 15px;
font-weight: 500;
color: #303133;
}
}
.metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 32px;
.metric-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px;
border: 1px solid #ebeef5;
border-radius: 4px;
background: #fafafa;
.metric-title {
font-size: 14px;
color: #606266;
margin-bottom: 12px;
}
.metric-value {
font-size: 24px;
font-weight: 600;
margin-bottom: 12px;
&.value-success {
color: #52c41a;
}
&.value-info {
color: #1890ff;
}
&.value-warning {
color: #fa8c16;
}
&.value-danger {
color: #f5222d;
}
}
.progress-ring-container {
width: 48px;
height: 48px;
.progress-ring {
width: 48px;
height: 48px;
transform: rotate(-90deg);
.progress-ring-bg {
stroke: #ebeef5;
}
.progress-ring-progress {
transition: stroke-dashoffset 0.5s ease;
}
}
}
}
}
}
}
// 上传弹窗样式
::v-deep .el-dialog__wrapper {
.upload-area {
width: 100%;
}
.el-upload {
width: 100%;
.el-upload-dragger {
width: 100%;
height: 200px;
}
}
.el-upload__tip {
margin-top: 8px;
color: #909399;
font-size: 12px;
}
}
// 响应式
@media (max-width: 1200px) {
.upload-section .upload-cards {
grid-template-columns: repeat(2, 1fr);
}
.quality-check-section .metrics {
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
}
@media (max-width: 768px) {
.upload-data-container {
padding: 8px;
}
.sidebar {
margin-bottom: 16px;
}
.content-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.upload-section .upload-cards {
grid-template-columns: 1fr;
}
.quality-check-section .metrics {
grid-template-columns: 1fr;
}
}
</style>