实现结果总览详情资产和征信页签

This commit is contained in:
wkc
2026-06-02 17:17:49 +08:00
parent 457e6c1d27
commit d45e9410ef
15 changed files with 433 additions and 85 deletions

View File

@@ -26,7 +26,7 @@ export function listCreditInfo(query) {
export function getCreditInfoDetail(personId) {
return request({
url: '/ccdi/creditInfo/' + personId,
url: '/ccdi/creditInfo/' + encodeURIComponent(personId),
method: 'get'
})
}

View File

@@ -0,0 +1,114 @@
<template>
<div class="credit-info-detail" v-loading="loading">
<div class="section-title">征信摘要</div>
<el-row :gutter="16" class="detail-summary">
<el-col :span="8">征信查询日期{{ formatQueryDate(detail.queryDate) }}</el-col>
<el-col :span="8">负债笔数{{ detail.debtCount || 0 }}</el-col>
<el-col :span="8">负债总额{{ formatAmount(detail.debtTotalAmount) }}</el-col>
</el-row>
<div class="section-title">负面信息</div>
<el-row :gutter="16" class="detail-summary">
<el-col :span="8">民事案件笔数{{ detail.negativeInfo.civilCnt || 0 }}</el-col>
<el-col :span="8">强制执行笔数{{ detail.negativeInfo.enforceCnt || 0 }}</el-col>
<el-col :span="8">行政处罚笔数{{ detail.negativeInfo.admCnt || 0 }}</el-col>
</el-row>
<el-row :gutter="16" class="detail-summary">
<el-col :span="8">民事案件金额{{ formatAmount(detail.negativeInfo.civilLmt) }}</el-col>
<el-col :span="8">强制执行金额{{ formatAmount(detail.negativeInfo.enforceLmt) }}</el-col>
<el-col :span="8">行政处罚金额{{ formatAmount(detail.negativeInfo.admLmt) }}</el-col>
</el-row>
<div class="section-title">负债信息</div>
<el-table :data="detail.debts || []" size="mini">
<template slot="empty">
<el-empty :image-size="80" description="暂无征信负债信息" />
</template>
<el-table-column label="负债大类" prop="debtMainType" min-width="120" />
<el-table-column label="负债小类" prop="debtSubType" min-width="120" />
<el-table-column label="债权人类型" prop="creditorType" min-width="120" />
<el-table-column label="负债名称" prop="debtName" min-width="180" />
<el-table-column label="负债本金余额" min-width="120">
<template slot-scope="scope">
{{ formatAmount(scope.row.principalBalance) }}
</template>
</el-table-column>
<el-table-column label="负债总额" min-width="120">
<template slot-scope="scope">
{{ formatAmount(scope.row.debtTotalAmount) }}
</template>
</el-table-column>
<el-table-column label="负债状态" prop="debtStatus" min-width="120" />
</el-table>
</div>
</template>
<script>
import { parseTime } from "@/utils/ruoyi";
export default {
name: "CreditInfoDetail",
props: {
detail: {
type: Object,
default: () => ({
queryDate: undefined,
debtCount: 0,
debtTotalAmount: 0,
negativeInfo: {},
debts: [],
}),
},
loading: {
type: Boolean,
default: false,
},
},
methods: {
formatQueryDate(value) {
if (!value) {
return "-";
}
if (typeof value === "string") {
const matched = value.match(/^(\d{4}-\d{2}-\d{2})/);
if (matched) {
return matched[1];
}
}
return parseTime(value, "{y}-{m}-{d}") || "-";
},
formatAmount(value) {
const amount = Number(value || 0);
if (!Number.isFinite(amount)) {
return "0";
}
return amount.toLocaleString("zh-CN", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
},
},
};
</script>
<style scoped>
.credit-info-detail {
min-height: 120px;
}
.section-title {
margin-bottom: 12px;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.section-title:not(:first-child) {
margin-top: 20px;
}
.detail-summary {
margin-bottom: 16px;
line-height: 32px;
}
</style>

View File

@@ -0,0 +1,25 @@
function sumDebtTotalAmount(debts) {
if (!Array.isArray(debts)) {
return 0;
}
return debts.reduce((total, item) => total + Number(item.debtTotalAmount || 0), 0);
}
export function normalizeCreditDetail(data = {}, row = {}) {
const negativeInfo = data.negativeInfo || {};
const debts = Array.isArray(data.debtList) ? data.debtList : [];
return {
personId: data.personId || row.idCard || "",
personName: data.personName || row.name || "",
idCard: data.idCard || data.personId || row.idCard || "",
queryDate: data.queryDate || negativeInfo.queryDate,
debtCount: debts.length,
debtTotalAmount: sumDebtTotalAmount(debts),
negativeInfo,
debts,
};
}
export function createEmptyCreditDetail() {
return normalizeCreditDetail();
}

View File

@@ -122,35 +122,7 @@
</el-dialog>
<el-dialog title="征信详情" :visible.sync="detailDialogVisible" width="960px" append-to-body>
<div class="section-title">征信摘要</div>
<el-row :gutter="16" class="detail-summary">
<el-col :span="8">征信查询日期{{ formatQueryDate(detailForm.queryDate) }}</el-col>
<el-col :span="8">负债笔数{{ detailForm.debtCount || 0 }}</el-col>
<el-col :span="8">负债总额{{ detailForm.debtTotalAmount || 0 }}</el-col>
</el-row>
<div class="section-title">负面信息</div>
<el-row :gutter="16" class="detail-summary">
<el-col :span="8">民事案件笔数{{ detailForm.civilCnt || 0 }}</el-col>
<el-col :span="8">强制执行笔数{{ detailForm.enforceCnt || 0 }}</el-col>
<el-col :span="8">行政处罚笔数{{ detailForm.admCnt || 0 }}</el-col>
</el-row>
<el-row :gutter="16" class="detail-summary">
<el-col :span="8">民事案件金额{{ detailForm.negativeInfo.civilLmt || 0 }}</el-col>
<el-col :span="8">强制执行金额{{ detailForm.negativeInfo.enforceLmt || 0 }}</el-col>
<el-col :span="8">行政处罚金额{{ detailForm.negativeInfo.admLmt || 0 }}</el-col>
</el-row>
<div class="section-title">负债信息</div>
<el-table :data="detailForm.debts || []" size="mini">
<el-table-column label="负债大类" prop="debtMainType" min-width="120" />
<el-table-column label="负债小类" prop="debtSubType" min-width="120" />
<el-table-column label="债权人类型" prop="creditorType" min-width="120" />
<el-table-column label="负债名称" prop="debtName" min-width="180" />
<el-table-column label="负债本金余额" prop="principalBalance" min-width="120" />
<el-table-column label="负债总额" prop="debtTotalAmount" min-width="120" />
<el-table-column label="负债状态" prop="debtStatus" min-width="120" />
</el-table>
<credit-info-detail :detail="detailForm" />
<div slot="footer" class="dialog-footer">
<el-button @click="detailDialogVisible = false"> </el-button>
@@ -166,9 +138,14 @@ import {
listCreditInfo,
uploadCreditHtml,
} from "@/api/ccdiCreditInfo";
import CreditInfoDetail from "./components/CreditInfoDetail.vue";
import { createEmptyCreditDetail, normalizeCreditDetail } from "./components/creditDetailViewModel.js";
export default {
name: "CcdiCreditInfo",
components: {
CreditInfoDetail,
},
data() {
return {
loading: false,
@@ -185,16 +162,7 @@ export default {
},
failureList: [],
detailDialogVisible: false,
detailForm: {
queryDate: undefined,
debtCount: 0,
debtTotalAmount: 0,
civilCnt: 0,
enforceCnt: 0,
admCnt: 0,
negativeInfo: {},
debts: [],
},
detailForm: createEmptyCreditDetail(),
queryParams: {
pageNum: 1,
pageSize: 10,
@@ -289,19 +257,7 @@ export default {
handleDetail(row) {
return getCreditInfoDetail(row.idCard).then((response) => {
const data = response.data || {};
const negativeInfo = data.negativeInfo || {};
this.detailForm = {
personId: data.personId || row.idCard,
personName: data.personName || row.name,
queryDate: data.queryDate || negativeInfo.queryDate || row.queryDate,
debtCount: row.debtCount || (data.debtList || []).length,
debtTotalAmount: row.debtTotalAmount || 0,
civilCnt: negativeInfo.civilCnt || row.civilCnt || 0,
enforceCnt: negativeInfo.enforceCnt || row.enforceCnt || 0,
admCnt: negativeInfo.admCnt || row.admCnt || 0,
negativeInfo,
debts: data.debtList || [],
};
this.detailForm = normalizeCreditDetail(data, row);
this.detailDialogVisible = true;
});
},

View File

@@ -54,10 +54,46 @@
/>
</el-tab-pane>
<el-tab-pane label="资产分析" name="assetAnalysis">
<project-analysis-placeholder-tab :tab-data="getTabData('assetAnalysis')" />
<div class="project-analysis-tab-panel">
<el-alert
v-if="assetError"
:closable="false"
class="project-analysis-layout__alert"
type="error"
show-icon
:title="assetError"
>
<template slot="default">
<el-button type="text" size="mini" @click="fetchAssetDetailData">重试</el-button>
</template>
</el-alert>
<family-asset-liability-detail
v-else
:detail="assetDetail"
:loading="assetLoading"
/>
</div>
</el-tab-pane>
<el-tab-pane label="征信摘要" name="creditSummary">
<project-analysis-placeholder-tab :tab-data="getTabData('creditSummary')" />
<el-tab-pane label="征信详情" name="creditDetail">
<div class="project-analysis-tab-panel">
<el-alert
v-if="creditError"
:closable="false"
class="project-analysis-layout__alert"
type="error"
show-icon
:title="creditError"
>
<template slot="default">
<el-button type="text" size="mini" @click="fetchCreditDetailData">重试</el-button>
</template>
</el-alert>
<credit-info-detail
v-else
:detail="creditDetail"
:loading="creditLoading"
/>
</div>
</el-tab-pane>
<el-tab-pane label="关系图谱" name="relationshipGraph">
<project-analysis-fund-flow-tab
@@ -87,19 +123,24 @@
</template>
<script>
import { getCreditInfoDetail } from "@/api/ccdiCreditInfo";
import { getFamilyAssetLiabilityDetail } from "@/api/ccdi/projectSpecialCheck";
import { getOverviewPersonAnalysisDetail } from "@/api/ccdi/projectOverview";
import CreditInfoDetail from "@/views/ccdiCreditInfo/components/CreditInfoDetail.vue";
import { createEmptyCreditDetail, normalizeCreditDetail } from "@/views/ccdiCreditInfo/components/creditDetailViewModel.js";
import FamilyAssetLiabilityDetail from "./FamilyAssetLiabilityDetail";
import ProjectAnalysisAbnormalTab from "./ProjectAnalysisAbnormalTab";
import ProjectAnalysisFundFlowTab from "./ProjectAnalysisFundFlowTab";
import ProjectAnalysisPlaceholderTab from "./ProjectAnalysisPlaceholderTab";
import ProjectAnalysisSidebar from "./ProjectAnalysisSidebar";
import { buildProjectAnalysisDialogData } from "./preliminaryCheck.mock";
export default {
name: "ProjectAnalysisDialog",
components: {
CreditInfoDetail,
FamilyAssetLiabilityDetail,
ProjectAnalysisAbnormalTab,
ProjectAnalysisFundFlowTab,
ProjectAnalysisPlaceholderTab,
ProjectAnalysisSidebar,
},
props: {
@@ -137,6 +178,14 @@ export default {
detailLoading: false,
detailError: "",
detailData: null,
assetLoading: false,
assetError: "",
assetDetail: null,
assetLoaded: false,
creditLoading: false,
creditError: "",
creditDetail: createEmptyCreditDetail(),
creditLoaded: false,
};
},
computed: {
@@ -175,6 +224,14 @@ export default {
this.fetchDetailData();
}
},
projectId() {
this.resetAssetDetailState();
this.resetCreditDetailState();
},
person() {
this.resetAssetDetailState();
this.resetCreditDetailState();
},
},
methods: {
resetDialogState() {
@@ -182,6 +239,20 @@ export default {
this.detailLoading = false;
this.detailError = "";
this.detailData = null;
this.resetAssetDetailState();
this.resetCreditDetailState();
},
resetAssetDetailState() {
this.assetLoading = false;
this.assetError = "";
this.assetDetail = null;
this.assetLoaded = false;
},
resetCreditDetailState() {
this.creditLoading = false;
this.creditError = "";
this.creditDetail = createEmptyCreditDetail();
this.creditLoaded = false;
},
resolveStaffIdCard() {
return (this.modelSummary && this.modelSummary.staffIdCard)
@@ -212,6 +283,55 @@ export default {
handleRetryDetail() {
this.fetchDetailData();
},
async fetchAssetDetailData() {
if (this.assetLoaded || this.assetLoading) {
return;
}
const staffIdCard = this.resolveStaffIdCard();
if (!this.projectId || !staffIdCard) {
this.assetError = "缺少项目或人员身份证号,无法加载资产详情";
return;
}
this.assetLoading = true;
this.assetError = "";
try {
const response = await getFamilyAssetLiabilityDetail(this.projectId, staffIdCard);
this.assetDetail = (response && response.data) || {};
this.assetLoaded = true;
} catch (error) {
this.assetDetail = null;
this.assetLoaded = false;
this.assetError = "资产详情加载失败,请稍后重试";
console.error("加载资产详情失败", error);
} finally {
this.assetLoading = false;
}
},
async fetchCreditDetailData() {
if (this.creditLoaded || this.creditLoading) {
return;
}
const staffIdCard = this.resolveStaffIdCard();
if (!staffIdCard) {
this.creditError = "缺少人员身份证号,无法加载征信详情";
return;
}
this.creditLoading = true;
this.creditError = "";
try {
const response = await getCreditInfoDetail(staffIdCard);
const data = (response && response.data) || {};
this.creditDetail = normalizeCreditDetail(data);
this.creditLoaded = true;
} catch (error) {
this.creditDetail = createEmptyCreditDetail();
this.creditLoaded = false;
this.creditError = "征信详情加载失败,请稍后重试";
console.error("加载征信详情失败", error);
} finally {
this.creditLoading = false;
}
},
getTabData(tabKey) {
return (
this.dialogData.tabs.find((item) => item.key === tabKey) || {
@@ -225,6 +345,12 @@ export default {
this.$emit("close");
},
handleTabChange() {
if (this.activeTab === "assetAnalysis") {
this.fetchAssetDetailData();
}
if (this.activeTab === "creditDetail") {
this.fetchCreditDetailData();
}
this.$nextTick(() => {
const tabRef = this.activeTab === "relationshipGraph"
? this.$refs.relationshipGraphTab

View File

@@ -193,12 +193,12 @@ export const projectAnalysisTabs = [
{
key: "assetAnalysis",
label: "资产分析",
description: "静态承载资产分析页签内容,本轮不接入新接口。",
description: "展示员工家庭资产负债专项核查资产详情。",
},
{
key: "creditSummary",
label: "征信摘要",
description: "静态承载征信摘要页签内容,本轮不接入新接口。",
key: "creditDetail",
label: "征信详情",
description: "展示征信信息维护中的征信详情。",
},
{
key: "relationshipGraph",

View File

@@ -15,6 +15,7 @@ const source = fs.readFileSync(apiPath, "utf8");
"/ccdi/creditInfo/upload",
"/ccdi/creditInfo/list",
"/ccdi/creditInfo/",
"encodeURIComponent(personId)",
].forEach((token) => {
assert(source.includes(token), `征信维护 API 缺少关键契约: ${token}`);
});

View File

@@ -6,16 +6,41 @@ const componentPath = path.resolve(
__dirname,
"../../src/views/ccdiCreditInfo/index.vue"
);
const detailComponentPath = path.resolve(
__dirname,
"../../src/views/ccdiCreditInfo/components/CreditInfoDetail.vue"
);
const helperPath = path.resolve(
__dirname,
"../../src/views/ccdiCreditInfo/components/creditDetailViewModel.js"
);
const source = fs.readFileSync(componentPath, "utf8");
assert(fs.existsSync(detailComponentPath), "征信详情展示组件未抽取");
const detailSource = fs.readFileSync(detailComponentPath, "utf8");
const helperSource = fs.readFileSync(helperPath, "utf8");
[
"formatQueryDate(value)",
"const matched = value.match(/^(",
'this.parseTime(value, "{y}-{m}-{d}")',
"{{ formatQueryDate(scope.row.queryDate) }}",
"{{ formatQueryDate(detailForm.queryDate) }}",
"normalizeCreditDetail(data, row)",
].forEach((token) => {
assert(source.includes(token), `征信时间展示缺少关键实现: ${token}`);
});
[
"formatQueryDate(value)",
"const matched = value.match(/^(",
'parseTime(value, "{y}-{m}-{d}")',
"{{ formatQueryDate(detail.queryDate) }}",
].forEach((token) => {
assert(detailSource.includes(token), `征信详情组件时间展示缺少关键实现: ${token}`);
});
[
"queryDate: data.queryDate || negativeInfo.queryDate",
"debtCount: debts.length",
"debtTotalAmount: sumDebtTotalAmount(debts)",
].forEach((token) => {
assert(helperSource.includes(token), `征信详情标准化缺少关键实现: ${token}`);
});
console.log("credit-info-date-display test passed");

View File

@@ -6,16 +6,21 @@ const componentPath = path.resolve(
__dirname,
"../../src/views/ccdiCreditInfo/index.vue"
);
const detailComponentPath = path.resolve(
__dirname,
"../../src/views/ccdiCreditInfo/components/CreditInfoDetail.vue"
);
const source = fs.readFileSync(componentPath, "utf8");
assert(fs.existsSync(detailComponentPath), "征信详情展示组件未抽取");
const detailSource = fs.readFileSync(detailComponentPath, "utf8");
[
"CreditInfoDetail",
"<credit-info-detail",
"detailDialogVisible",
"detailForm",
"负债信息",
"负面信息",
"civilCnt",
"enforceCnt",
"admCnt",
"normalizeCreditDetail",
"getCreditInfoDetail(row.idCard)",
"handleDetail",
"handleDelete",
"deleteCreditInfo",
@@ -24,4 +29,19 @@ const source = fs.readFileSync(componentPath, "utf8");
assert(source.includes(token), `详情或删除交互缺少关键结构: ${token}`);
});
[
'name: "CreditInfoDetail"',
"征信摘要",
"负债信息",
"负面信息",
"detail.debtCount",
"detail.debtTotalAmount",
"detail.negativeInfo.civilCnt",
"detail.negativeInfo.enforceCnt",
"detail.negativeInfo.admCnt",
"detail.debts || []",
].forEach((token) => {
assert(detailSource.includes(token), `征信详情组件缺少关键结构: ${token}`);
});
console.log("credit-info-detail-ui test passed");

View File

@@ -12,7 +12,8 @@ assert(fs.existsSync(componentPath), "未找到征信维护页面 index.vue");
const source = fs.readFileSync(componentPath, "utf8");
[
"征信维护",
'name: "CcdiCreditInfo"',
"app-container",
"姓名",
"身份证号",
"批量上传征信HTML",

View File

@@ -57,7 +57,7 @@ assert(!dialog.includes("project-analysis-layout__main-scroll"), "主区不应
"summary",
"extraFields",
"grid-template-columns: minmax(0, 1fr)",
"border-radius: 6px",
"align-items: flex-start",
].forEach((token) => assert(abnormalTab.includes(token), token));
assert(!abnormalTab.includes("border-radius: 12px"), "异常明细内部不应继续使用独立 12px 圆角");

View File

@@ -27,26 +27,33 @@ const mockSource = fs.readFileSync(
'name="abnormalDetail"',
'label="异常明细"',
'label="资产分析"',
'label="征信摘要"',
'label="征信详情"',
'label="关系图谱"',
'label="资金流向"',
"<family-asset-liability-detail",
"<credit-info-detail",
"fetchAssetDetailData()",
"fetchCreditDetailData()",
"getFamilyAssetLiabilityDetail",
"getCreditInfoDetail",
"fetchDetailData()",
"detailLoading",
"detailError",
"handleRetryDetail()",
"background: #f5f7fb",
"border: 1px solid #dbe4ef",
"border-radius: 8px",
"assetLoaded",
"creditLoaded",
"缺少项目或人员身份证号,无法加载资产详情",
"缺少人员身份证号,无法加载征信详情",
].forEach((token) => assert(dialog.includes(token), token));
[
'width="92%"',
'width="88%"',
'top="2vh"',
"project-analysis-header__main",
"project-analysis-header__meta",
"project-analysis-layout__main",
"flex: 0 0 320px",
"border-radius: 6px",
"flex: 0 0 34%",
"border-radius: 2px",
].forEach((token) => assert(dialog.includes(token), token));
[
@@ -65,7 +72,14 @@ const mockSource = fs.readFileSync(
"projectAnalysisTabs",
'key: "abnormalDetail"',
'key: "assetAnalysis"',
'key: "creditSummary"',
'key: "creditDetail"',
'label: "征信详情"',
'key: "relationshipGraph"',
'key: "fundFlow"',
].forEach((token) => assert(mockSource.includes(token), token));
[
'label="征信摘要"',
'key: "creditSummary"',
"静态承载征信摘要页签内容,本轮不接入新接口。",
].forEach((token) => assert(!dialog.includes(token) && !mockSource.includes(token), token));

View File

@@ -35,9 +35,8 @@ const entry = fs.readFileSync(
"formatRiskTag",
"tag.ruleName",
"flex-wrap: wrap",
"align-items: flex-start",
"border: 1px solid #dbe4ef",
"border-radius: 6px",
"border: 1px solid #dde3ec",
"border-radius: 3px",
].forEach((token) => assert(sidebar.includes(token), token));
assert(!sidebar.includes("当前命中模型"), "命中模型摘要应移除当前命中模型字段");
@@ -45,7 +44,6 @@ assert(!sidebar.includes("排查记录摘要"), "侧栏应移除排查记录摘
assert(!sidebar.includes("position: sticky"), "左侧整卡不应保持固定");
assert(!sidebar.includes("border-radius: 20px"), "侧栏不应继续保留旧大圆角卡片样式");
assert(!sidebar.includes("background: rgba(255, 255, 255, 0.9)"), "侧栏不应继续保留旧半透明卡片底色");
assert(!sidebar.includes("justify-content: space-between"), "不应继续以表单式左右对齐作为主体布局");
assert(!sidebar.includes("关系人画像"), "侧栏不应扩展到额外区块");
assert(!sidebar.includes("资产分布"), "侧栏不应扩展到额外区块");