feat: 完成上传数据页面

This commit is contained in:
mengke
2026-03-02 19:18:45 +08:00
parent 7d1ab61705
commit c6d5063c8d
6 changed files with 1612 additions and 76 deletions

View File

@@ -0,0 +1,947 @@
<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 {
getUploadStatus,
uploadFile,
deleteFile,
getNameListOptions,
updateNameListSelection,
executeQualityCheck,
pullBankInfo,
generateReport,
getImportStatus,
} 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: "employee",
title: "员工关系导入",
desc: "Excel 表格上传员工家庭关系信息",
icon: "el-icon-user",
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(4, 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>