完善外部人员预警与项目分析上线内容
This commit is contained in:
@@ -20,6 +20,26 @@ export function getOverviewRiskPeople(params) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getOverviewExternalPersons(params) {
|
||||
return request({
|
||||
url: '/ccdi/project/overview/external-persons',
|
||||
method: 'get',
|
||||
params: {
|
||||
projectId: params.projectId,
|
||||
pageNum: params.pageNum,
|
||||
pageSize: params.pageSize
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function getOverviewExternalRiskSummary(projectId) {
|
||||
return request({
|
||||
url: '/ccdi/project/overview/external-persons/summary',
|
||||
method: 'get',
|
||||
params: { projectId }
|
||||
})
|
||||
}
|
||||
|
||||
export function getOverviewRiskModelCards(projectId) {
|
||||
return request({
|
||||
url: '/ccdi/project/overview/risk-models/cards',
|
||||
@@ -28,6 +48,14 @@ export function getOverviewRiskModelCards(projectId) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getOverviewExternalRiskModelCards(projectId) {
|
||||
return request({
|
||||
url: '/ccdi/project/overview/external-risk-models/cards',
|
||||
method: 'get',
|
||||
params: { projectId }
|
||||
})
|
||||
}
|
||||
|
||||
export function getOverviewRiskModelPeople(params) {
|
||||
return request({
|
||||
url: '/ccdi/project/overview/risk-models/people',
|
||||
@@ -44,6 +72,21 @@ export function getOverviewRiskModelPeople(params) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getOverviewExternalRiskModelPeople(params) {
|
||||
return request({
|
||||
url: '/ccdi/project/overview/external-risk-models/people',
|
||||
method: 'get',
|
||||
params: {
|
||||
projectId: params.projectId,
|
||||
modelCodes: params.modelCodes,
|
||||
matchMode: params.matchMode,
|
||||
keyword: params.keyword,
|
||||
pageNum: params.pageNum,
|
||||
pageSize: params.pageSize
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function getOverviewPersonAnalysisDetail(params) {
|
||||
return request({
|
||||
url: '/ccdi/project/overview/person-analysis/detail',
|
||||
|
||||
@@ -130,6 +130,7 @@ export function download(url, params, filename, config) {
|
||||
transformRequest: [(params) => { return tansParams(params) }],
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
responseType: 'blob',
|
||||
timeout: 120000,
|
||||
...config
|
||||
}).then(async (data) => {
|
||||
const isBlob = blobValidate(data)
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
<div class="shell-header">
|
||||
<div class="shell-title-group">
|
||||
<span class="shell-title">流水明细查询</span>
|
||||
<span class="shell-subtitle">按项目范围查询交易明细并查看详情</span>
|
||||
</div>
|
||||
<el-button
|
||||
size="small"
|
||||
@@ -164,6 +163,14 @@
|
||||
导出流水
|
||||
</el-button>
|
||||
</div>
|
||||
<el-alert
|
||||
v-if="prefillTip"
|
||||
:closable="false"
|
||||
class="prefill-alert"
|
||||
show-icon
|
||||
type="info"
|
||||
:title="prefillTip"
|
||||
/>
|
||||
|
||||
<el-tabs v-model="activeTab" class="result-tabs" @tab-click="handleTabChange">
|
||||
<el-tab-pane label="全部" name="all" />
|
||||
@@ -499,6 +506,10 @@ export default {
|
||||
projectStatus: "0",
|
||||
}),
|
||||
},
|
||||
detailQueryPrefill: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -511,6 +522,7 @@ export default {
|
||||
list: [],
|
||||
total: 0,
|
||||
listError: "",
|
||||
prefillTip: "",
|
||||
detailData: createEmptyDetailData(),
|
||||
queryParams: createDefaultQueryParams(this.projectId),
|
||||
optionData: createEmptyOptionData(),
|
||||
@@ -530,6 +542,13 @@ export default {
|
||||
this.getOptions();
|
||||
this.getList();
|
||||
},
|
||||
detailQueryPrefill: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler(value) {
|
||||
this.applyDetailQueryPrefill(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
buildFlowEvidenceFingerprint,
|
||||
@@ -596,10 +615,25 @@ export default {
|
||||
this.activeTab = "all";
|
||||
this.dateRange = [];
|
||||
this.queryParams = createDefaultQueryParams(this.projectId);
|
||||
this.prefillTip = "";
|
||||
this.syncProjectId();
|
||||
this.getOptions();
|
||||
this.getList();
|
||||
},
|
||||
applyDetailQueryPrefill(value) {
|
||||
if (!value || !Array.isArray(value.ourCertNos) || !value.ourCertNos.length) {
|
||||
return;
|
||||
}
|
||||
this.activeTab = "all";
|
||||
this.dateRange = [];
|
||||
this.queryParams = {
|
||||
...createDefaultQueryParams(this.projectId),
|
||||
ourCertNos: value.ourCertNos,
|
||||
};
|
||||
this.prefillTip = `当前查看:${value.title || "外部人员"} 的本方流水`;
|
||||
this.syncProjectId();
|
||||
this.getList();
|
||||
},
|
||||
handleTabChange(tab) {
|
||||
const tabName = tab && tab.name ? tab.name : this.activeTab;
|
||||
this.activeTab = tabName;
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
title="外部人员详情"
|
||||
:visible.sync="visibleProxy"
|
||||
width="88%"
|
||||
top="2vh"
|
||||
append-to-body
|
||||
custom-class="project-analysis-dialog external-person-analysis-dialog"
|
||||
@close="handleClosed"
|
||||
>
|
||||
<div class="project-analysis-dialog__body external-analysis-dialog__body">
|
||||
<div class="project-analysis-header">
|
||||
<div class="project-analysis-header__main">
|
||||
<div class="project-analysis-header__title-group">
|
||||
<div class="project-analysis-header__eyebrow">结果总览</div>
|
||||
<div class="project-analysis-header__title">外部人员详情</div>
|
||||
</div>
|
||||
<button class="project-analysis-header__close" type="button" aria-label="关闭" @click="closeDialog">
|
||||
<i class="el-icon-close" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-analysis-workspace">
|
||||
<project-analysis-sidebar
|
||||
class="project-analysis-layout__sidebar"
|
||||
:sidebar-data="sidebarData"
|
||||
:field-labels="sidebarFieldLabels"
|
||||
/>
|
||||
|
||||
<div class="project-analysis-layout__main">
|
||||
<el-alert
|
||||
v-if="detailError"
|
||||
:closable="false"
|
||||
class="external-detail-alert"
|
||||
type="error"
|
||||
show-icon
|
||||
:title="detailError"
|
||||
/>
|
||||
<el-tabs v-model="activeTab" class="project-analysis-tabs" stretch @tab-click="handleTabChange">
|
||||
<el-tab-pane label="异常明细" name="abnormalDetail">
|
||||
<div v-if="detailLoading" v-loading="detailLoading" class="external-detail-loading" />
|
||||
<div v-else>
|
||||
<project-analysis-abnormal-tab
|
||||
:detail-data="abnormalDetailData"
|
||||
:person="normalizedPerson"
|
||||
:project-id="projectId"
|
||||
@evidence-confirm="$emit('evidence-confirm', $event)"
|
||||
/>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="资金流向" name="fund">
|
||||
<project-analysis-fund-flow-tab
|
||||
v-if="activeTab === 'fund'"
|
||||
ref="fundFlowTab"
|
||||
:project-id="projectId"
|
||||
:person="normalizedPerson"
|
||||
:model-summary="fundModelSummary"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listBankStatement } from "@/api/ccdiProjectBankStatement";
|
||||
import ProjectAnalysisAbnormalTab from "./ProjectAnalysisAbnormalTab";
|
||||
import ProjectAnalysisFundFlowTab from "./ProjectAnalysisFundFlowTab";
|
||||
import ProjectAnalysisSidebar from "./ProjectAnalysisSidebar";
|
||||
|
||||
export default {
|
||||
name: "ExternalPersonDetailDialog",
|
||||
components: {
|
||||
ProjectAnalysisAbnormalTab,
|
||||
ProjectAnalysisFundFlowTab,
|
||||
ProjectAnalysisSidebar,
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
person: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
projectId: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
projectName: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: "abnormalDetail",
|
||||
detailLoading: false,
|
||||
detailError: "",
|
||||
statementRows: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
visibleProxy: {
|
||||
get() {
|
||||
return this.visible;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit("update:visible", value);
|
||||
},
|
||||
},
|
||||
displayName() {
|
||||
return (this.person && (this.person.name || this.person.staffName)) || "外部人员";
|
||||
},
|
||||
displayCertNo() {
|
||||
return (this.person && (this.person.idNo || this.person.staffIdCard)) || "-";
|
||||
},
|
||||
displaySubjectType() {
|
||||
return (this.person && (this.person.subjectType || this.person.staffCode)) || "外部人员";
|
||||
},
|
||||
relatedObject() {
|
||||
return (this.person && (this.person.relatedObject || this.person.department)) || "-";
|
||||
},
|
||||
normalizedPerson() {
|
||||
return {
|
||||
...(this.person || {}),
|
||||
name: this.displayName,
|
||||
staffName: this.displayName,
|
||||
idNo: this.displayCertNo,
|
||||
staffIdCard: this.displayCertNo,
|
||||
staffCode: this.displaySubjectType,
|
||||
department: this.relatedObject,
|
||||
};
|
||||
},
|
||||
riskTags() {
|
||||
if (Array.isArray(this.person && this.person.riskPointTagList)) {
|
||||
return this.person.riskPointTagList;
|
||||
}
|
||||
if (Array.isArray(this.person && this.person.hitTagList)) {
|
||||
return this.person.hitTagList;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
statementTagKeys() {
|
||||
const keys = new Set();
|
||||
this.statementRows.forEach((row) => {
|
||||
if (!Array.isArray(row && row.hitTags)) {
|
||||
return;
|
||||
}
|
||||
row.hitTags.forEach((tag) => {
|
||||
this.buildTagKeys(tag).forEach((key) => keys.add(key));
|
||||
});
|
||||
});
|
||||
return keys;
|
||||
},
|
||||
sidebarFieldLabels() {
|
||||
return {
|
||||
staffCode: "主体类型",
|
||||
department: "证件号",
|
||||
projectName: "所属项目",
|
||||
};
|
||||
},
|
||||
sidebarData() {
|
||||
return {
|
||||
basicInfo: {
|
||||
name: this.displayName,
|
||||
riskLevel: (this.person && this.person.riskLevel) || "-",
|
||||
staffCode: this.displaySubjectType,
|
||||
department: this.displayCertNo,
|
||||
projectName: this.projectName || "-",
|
||||
},
|
||||
modelSummary: {
|
||||
modelCount: (this.person && this.person.modelCount) || 0,
|
||||
riskTags: this.riskTags,
|
||||
},
|
||||
};
|
||||
},
|
||||
abnormalDetailData() {
|
||||
const statementGroup = this.buildStatementGroup();
|
||||
const objectRecords = this.riskTags
|
||||
.filter((tag) => !this.hasMatchedStatementTag(tag))
|
||||
.map((tag, index) => this.buildObjectAbnormalRecord(tag, index));
|
||||
const groups = [];
|
||||
if (statementGroup.records.length) {
|
||||
groups.push(statementGroup);
|
||||
}
|
||||
if (objectRecords.length) {
|
||||
groups.push({
|
||||
groupCode: "EXTERNAL_OBJECT_WARNING",
|
||||
groupName: "对象异常明细",
|
||||
groupType: "OBJECT",
|
||||
records: objectRecords,
|
||||
});
|
||||
}
|
||||
return {
|
||||
groups,
|
||||
};
|
||||
},
|
||||
fundModelSummary() {
|
||||
return {
|
||||
staffIdCard: this.displayCertNo,
|
||||
modelCount: (this.person && this.person.modelCount) || this.riskTags.length,
|
||||
riskTags: this.riskTags,
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(value) {
|
||||
if (value) {
|
||||
this.activeTab = "abnormalDetail";
|
||||
this.loadStatementRows();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async loadStatementRows() {
|
||||
if (!this.projectId || !this.displayCertNo || this.displayCertNo === "-") {
|
||||
this.statementRows = [];
|
||||
return;
|
||||
}
|
||||
this.statementRows = [];
|
||||
this.detailLoading = true;
|
||||
this.detailError = "";
|
||||
try {
|
||||
const response = await listBankStatement({
|
||||
projectId: this.projectId,
|
||||
ourCertNos: [this.displayCertNo],
|
||||
pageNum: 1,
|
||||
pageSize: 200,
|
||||
});
|
||||
this.statementRows = Array.isArray(response && response.rows) ? response.rows : [];
|
||||
} catch (error) {
|
||||
this.statementRows = [];
|
||||
this.detailError = "外部人员流水异常明细加载失败,请稍后重试";
|
||||
console.error("加载外部人员流水异常明细失败", error);
|
||||
} finally {
|
||||
this.detailLoading = false;
|
||||
}
|
||||
},
|
||||
buildStatementGroup() {
|
||||
const records = this.statementRows
|
||||
.map((row) => this.normalizeStatementRow(row))
|
||||
.filter((row) => row.hitTags.length);
|
||||
return {
|
||||
groupCode: "EXTERNAL_BANK_STATEMENT",
|
||||
groupName: "流水异常明细",
|
||||
groupType: "BANK_STATEMENT",
|
||||
records,
|
||||
};
|
||||
},
|
||||
normalizeStatementRow(row) {
|
||||
const hitTags = Array.isArray(row && row.hitTags)
|
||||
? row.hitTags.filter((tag) => this.isMatchedRiskTag(tag))
|
||||
.map((tag) => this.enrichStatementTag(tag))
|
||||
: [];
|
||||
return {
|
||||
...row,
|
||||
hitTags,
|
||||
};
|
||||
},
|
||||
isMatchedRiskTag(statementTag) {
|
||||
const statementKeys = this.buildTagKeys(statementTag);
|
||||
return this.riskTags.some((riskTag) => {
|
||||
const riskKeys = this.buildTagKeys(riskTag);
|
||||
return statementKeys.some((key) => riskKeys.includes(key));
|
||||
});
|
||||
},
|
||||
hasMatchedStatementTag(riskTag) {
|
||||
const riskKeys = this.buildTagKeys(riskTag);
|
||||
return riskKeys.some((key) => this.statementTagKeys.has(key));
|
||||
},
|
||||
buildTagKeys(tag) {
|
||||
if (!tag) {
|
||||
return [];
|
||||
}
|
||||
if (typeof tag === "string") {
|
||||
return [`name:${tag}`];
|
||||
}
|
||||
return [
|
||||
tag.ruleCode ? `code:${tag.ruleCode}` : "",
|
||||
tag.ruleName ? `name:${tag.ruleName}` : "",
|
||||
].filter(Boolean);
|
||||
},
|
||||
enrichStatementTag(tag) {
|
||||
const matched = this.riskTags.find((item) => item && (
|
||||
(tag.ruleCode && item.ruleCode === tag.ruleCode)
|
||||
|| (tag.ruleName && item.ruleName === tag.ruleName)
|
||||
));
|
||||
return {
|
||||
...tag,
|
||||
modelCode: tag.modelCode || (matched && matched.modelCode),
|
||||
modelName: tag.modelName || (matched && matched.modelName),
|
||||
};
|
||||
},
|
||||
buildObjectAbnormalRecord(tag, index) {
|
||||
const safeTag = typeof tag === "object" && tag ? tag : {};
|
||||
const modelName = safeTag.modelName || "外部人员模型";
|
||||
const ruleName = safeTag.ruleName || this.formatRiskTag(tag) || "外部人员预警";
|
||||
return {
|
||||
modelCode: safeTag.modelCode || `EXTERNAL_MODEL_${index}`,
|
||||
title: ruleName,
|
||||
subtitle: modelName,
|
||||
riskTags: [modelName, this.formatRiskLevel(safeTag.riskLevel)].filter(Boolean),
|
||||
reasonDetail: safeTag.reasonDetail || this.buildReasonDetail(ruleName, modelName),
|
||||
summary: ruleName,
|
||||
extraFields: [],
|
||||
};
|
||||
},
|
||||
buildReasonDetail(ruleName, modelName) {
|
||||
const parts = [
|
||||
this.displayName,
|
||||
`命中${modelName}`,
|
||||
ruleName,
|
||||
].filter(Boolean);
|
||||
return parts.join(",");
|
||||
},
|
||||
formatRiskLevel(value) {
|
||||
if (value === "HIGH") {
|
||||
return "高风险";
|
||||
}
|
||||
if (value === "MEDIUM") {
|
||||
return "中风险";
|
||||
}
|
||||
if (value === "LOW") {
|
||||
return "低风险";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
formatRiskTag(tag) {
|
||||
if (typeof tag === "string") {
|
||||
return tag;
|
||||
}
|
||||
if (tag && typeof tag === "object") {
|
||||
return tag.ruleName || tag.modelName || tag.label || tag.name || "";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
closeDialog() {
|
||||
this.visibleProxy = false;
|
||||
},
|
||||
handleClosed() {
|
||||
this.activeTab = "abnormalDetail";
|
||||
this.detailLoading = false;
|
||||
this.detailError = "";
|
||||
this.statementRows = [];
|
||||
},
|
||||
handleTabChange() {
|
||||
this.$nextTick(() => {
|
||||
const fundFlowTab = this.$refs.fundFlowTab;
|
||||
if (fundFlowTab && fundFlowTab.ensureGraphReady) {
|
||||
fundFlowTab.ensureGraphReady();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.project-analysis-dialog__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(92vh - 64px);
|
||||
border: 1px solid #dde3ec;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-analysis-header {
|
||||
padding: 28px 40px 24px;
|
||||
border-bottom: 1px solid #dde3ec;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.project-analysis-header__main {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.project-analysis-header__title-group {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.project-analysis-header__eyebrow {
|
||||
color: #65758d;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.project-analysis-header__title {
|
||||
margin-top: 18px;
|
||||
color: #101a2b;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.project-analysis-header__close {
|
||||
flex: 0 0 auto;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
background: transparent;
|
||||
color: #8d99aa;
|
||||
font-size: 18px;
|
||||
line-height: 34px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.project-analysis-header__close:hover {
|
||||
background: #f2f5f8;
|
||||
color: #245b8f;
|
||||
}
|
||||
|
||||
.project-analysis-workspace {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 36px;
|
||||
min-height: 640px;
|
||||
max-height: calc(92vh - 150px);
|
||||
padding: 20px 40px 28px;
|
||||
overflow: auto;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.project-analysis-layout__sidebar {
|
||||
flex: 0 0 34%;
|
||||
max-width: 460px;
|
||||
}
|
||||
|
||||
.project-analysis-layout__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-left: 1px solid #dde3ec;
|
||||
padding-left: 36px;
|
||||
}
|
||||
|
||||
.project-analysis-tabs {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.external-detail-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.external-detail-loading {
|
||||
min-height: 360px;
|
||||
border: 1px solid #d9e1ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
</style>
|
||||
@@ -12,6 +12,7 @@
|
||||
<div class="stats-content">
|
||||
<div class="stats-label">{{ item.label }}</div>
|
||||
<div class="stats-value">{{ item.value }}</div>
|
||||
<div v-if="item.desc" class="stats-desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,4 +99,12 @@ export default {
|
||||
font-weight: 700;
|
||||
color: var(--ccdi-text-primary);
|
||||
}
|
||||
|
||||
.stats-desc {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
color: var(--ccdi-text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -30,18 +30,21 @@
|
||||
导出报告
|
||||
</el-button>
|
||||
</div>
|
||||
<overview-stats :summary="currentData.summary" />
|
||||
<overview-stats :summary="visibleSummary" />
|
||||
<risk-people-section
|
||||
:project-id="projectId"
|
||||
:section-data="currentData.riskPeople"
|
||||
:selected-model-codes="selectedModelCodes"
|
||||
@view-project-analysis="handleRiskPeopleProjectAnalysis"
|
||||
@view-external-detail="handleExternalDetail"
|
||||
@scope-summary-change="handleRiskPeopleScopeSummaryChange"
|
||||
/>
|
||||
</section>
|
||||
<risk-model-section
|
||||
:section-data="currentData.riskModels"
|
||||
@selection-change="handleRiskModelSelectionChange"
|
||||
@view-project-analysis="handleRiskModelProjectAnalysis"
|
||||
@view-external-detail="handleExternalDetail"
|
||||
/>
|
||||
<risk-detail-section
|
||||
:section-data="currentData.riskDetails"
|
||||
@@ -58,6 +61,13 @@
|
||||
@close="handleProjectAnalysisDialogClose"
|
||||
@evidence-confirm="$emit('evidence-confirm', $event)"
|
||||
/>
|
||||
<external-person-detail-dialog
|
||||
:visible.sync="externalDetailVisible"
|
||||
:person="currentExternalPerson"
|
||||
:project-id="projectId"
|
||||
:project-name="projectInfo.projectName"
|
||||
@evidence-confirm="$emit('evidence-confirm', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -70,6 +80,7 @@ import {
|
||||
import {
|
||||
getOverviewDashboard,
|
||||
getOverviewEmployeeCreditNegative,
|
||||
getOverviewExternalRiskSummary,
|
||||
getOverviewRiskPeople,
|
||||
getOverviewRiskModelCards,
|
||||
getOverviewSuspiciousTransactions,
|
||||
@@ -79,6 +90,7 @@ import RiskPeopleSection from "./RiskPeopleSection";
|
||||
import RiskModelSection from "./RiskModelSection";
|
||||
import RiskDetailSection from "./RiskDetailSection";
|
||||
import ProjectAnalysisDialog from "./ProjectAnalysisDialog";
|
||||
import ExternalPersonDetailDialog from "./ExternalPersonDetailDialog";
|
||||
|
||||
export default {
|
||||
name: "PreliminaryCheck",
|
||||
@@ -88,6 +100,7 @@ export default {
|
||||
RiskModelSection,
|
||||
RiskDetailSection,
|
||||
ProjectAnalysisDialog,
|
||||
ExternalPersonDetailDialog,
|
||||
},
|
||||
props: {
|
||||
projectId: {
|
||||
@@ -118,6 +131,15 @@ export default {
|
||||
currentModel: "-",
|
||||
riskTags: [],
|
||||
},
|
||||
riskPeopleScopeSummary: {
|
||||
activeTab: "employee",
|
||||
external: {
|
||||
total: 0,
|
||||
rows: [],
|
||||
},
|
||||
},
|
||||
currentExternalPerson: null,
|
||||
externalDetailVisible: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -127,6 +149,12 @@ export default {
|
||||
}
|
||||
return this.stateDataMap[this.pageState] || this.realData;
|
||||
},
|
||||
visibleSummary() {
|
||||
return {
|
||||
...this.currentData.summary,
|
||||
stats: this.buildCombinedSummaryStats(),
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
projectId(newVal) {
|
||||
@@ -138,6 +166,7 @@ export default {
|
||||
this.pageState = "empty";
|
||||
this.selectedModelCodes = [];
|
||||
this.resetProjectAnalysisDialog();
|
||||
this.resetExternalDetailDialog();
|
||||
},
|
||||
},
|
||||
created() {
|
||||
@@ -148,17 +177,90 @@ export default {
|
||||
this.realData = this.stateDataMap.empty;
|
||||
this.pageState = "empty";
|
||||
this.resetProjectAnalysisDialog();
|
||||
this.resetExternalDetailDialog();
|
||||
},
|
||||
methods: {
|
||||
handleRiskModelSelectionChange(modelCodes) {
|
||||
this.selectedModelCodes = Array.isArray(modelCodes) ? [...modelCodes] : [];
|
||||
},
|
||||
handleRiskPeopleScopeSummaryChange(summary) {
|
||||
this.riskPeopleScopeSummary = {
|
||||
activeTab: summary && summary.activeTab ? summary.activeTab : "employee",
|
||||
external: {
|
||||
total: summary && summary.external ? summary.external.total || 0 : 0,
|
||||
rows: summary && summary.external && Array.isArray(summary.external.rows)
|
||||
? summary.external.rows
|
||||
: [],
|
||||
},
|
||||
};
|
||||
},
|
||||
buildCombinedSummaryStats() {
|
||||
const external = (this.currentData.summary && this.currentData.summary.externalRiskSummary) || {};
|
||||
const employeeStats = ((this.currentData.summary && this.currentData.summary.stats) || []);
|
||||
const statMap = employeeStats.reduce((result, item) => {
|
||||
result[item.key] = item;
|
||||
return result;
|
||||
}, {});
|
||||
const high = Number((statMap.riskPeople && statMap.riskPeople.value) || 0);
|
||||
const medium = Number((statMap.medium && statMap.medium.value) || 0);
|
||||
const low = Number((statMap.low && statMap.low.value) || 0);
|
||||
const noRisk = Number((statMap.count && statMap.count.value) || 0);
|
||||
const externalTotal = Number(external.total || 0);
|
||||
const externalHigh = Number(external.high || 0);
|
||||
const externalMedium = Number(external.medium || 0);
|
||||
const externalLow = Number(external.low || 0);
|
||||
const externalNoRisk = Number(external.noRisk || 0);
|
||||
const buildSplit = (employeeValue, externalValue) => (externalTotal > 0
|
||||
? `员工 ${employeeValue} / 外部 ${externalValue}`
|
||||
: "");
|
||||
return [
|
||||
{
|
||||
...(statMap.people || {}),
|
||||
key: "people",
|
||||
label: "总人数",
|
||||
value: Number((statMap.people && statMap.people.value) || 0) + externalTotal,
|
||||
desc: buildSplit(Number((statMap.people && statMap.people.value) || 0), externalTotal),
|
||||
},
|
||||
{
|
||||
...(statMap.riskPeople || {}),
|
||||
key: "riskPeople",
|
||||
label: "高风险",
|
||||
value: high + externalHigh,
|
||||
desc: buildSplit(high, externalHigh),
|
||||
},
|
||||
{
|
||||
...(statMap.medium || {}),
|
||||
key: "medium",
|
||||
label: "中风险",
|
||||
value: medium + externalMedium,
|
||||
desc: buildSplit(medium, externalMedium),
|
||||
},
|
||||
{
|
||||
...(statMap.low || {}),
|
||||
key: "low",
|
||||
label: "低风险",
|
||||
value: low + externalLow,
|
||||
desc: buildSplit(low, externalLow),
|
||||
},
|
||||
{
|
||||
...(statMap.count || {}),
|
||||
key: "count",
|
||||
label: "无风险",
|
||||
value: noRisk + externalNoRisk,
|
||||
desc: buildSplit(noRisk, externalNoRisk),
|
||||
},
|
||||
];
|
||||
},
|
||||
handleRiskPeopleProjectAnalysis(row) {
|
||||
this.openProjectAnalysisDialog("riskPeople", row);
|
||||
},
|
||||
handleRiskModelProjectAnalysis(row) {
|
||||
this.openProjectAnalysisDialog("riskModelPeople", row);
|
||||
},
|
||||
handleExternalDetail(row) {
|
||||
this.currentExternalPerson = row || null;
|
||||
this.externalDetailVisible = true;
|
||||
},
|
||||
openProjectAnalysisDialog(source, person) {
|
||||
this.projectAnalysisSource = source || "riskPeople";
|
||||
this.currentProjectAnalysisPerson = person || null;
|
||||
@@ -201,6 +303,10 @@ export default {
|
||||
riskTags,
|
||||
};
|
||||
},
|
||||
resetExternalDetailDialog() {
|
||||
this.externalDetailVisible = false;
|
||||
this.currentExternalPerson = null;
|
||||
},
|
||||
handleOverviewReportExport() {
|
||||
if (!this.projectId) {
|
||||
return;
|
||||
@@ -219,14 +325,30 @@ export default {
|
||||
this.pageState = "empty";
|
||||
this.selectedModelCodes = [];
|
||||
this.resetProjectAnalysisDialog();
|
||||
this.resetExternalDetailDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
this.pageState = "loading";
|
||||
this.selectedModelCodes = [];
|
||||
this.riskPeopleScopeSummary = {
|
||||
activeTab: "employee",
|
||||
external: {
|
||||
total: 0,
|
||||
rows: [],
|
||||
},
|
||||
};
|
||||
this.resetProjectAnalysisDialog();
|
||||
this.resetExternalDetailDialog();
|
||||
try {
|
||||
const [dashboardRes, riskPeopleRes, riskModelCardsRes, suspiciousRes, creditNegativeRes] = await Promise.all([
|
||||
const [
|
||||
dashboardRes,
|
||||
riskPeopleRes,
|
||||
riskModelCardsRes,
|
||||
suspiciousRes,
|
||||
creditNegativeRes,
|
||||
externalSummaryRes,
|
||||
] = await Promise.all([
|
||||
getOverviewDashboard(this.projectId),
|
||||
getOverviewRiskPeople({
|
||||
projectId: this.projectId,
|
||||
@@ -245,12 +367,14 @@ export default {
|
||||
pageNum: 1,
|
||||
pageSize: 5,
|
||||
}),
|
||||
getOverviewExternalRiskSummary(this.projectId),
|
||||
]);
|
||||
const dashboardData = (dashboardRes && dashboardRes.data) || {};
|
||||
const riskPeopleData = (riskPeopleRes && riskPeopleRes.data) || {};
|
||||
const riskModelCardsData = (riskModelCardsRes && riskModelCardsRes.data) || {};
|
||||
const suspiciousData = (suspiciousRes && suspiciousRes.data) || {};
|
||||
const creditNegativeData = (creditNegativeRes && creditNegativeRes.data) || {};
|
||||
const externalRiskSummary = (externalSummaryRes && externalSummaryRes.data) || {};
|
||||
|
||||
this.realData = createOverviewLoadedData({
|
||||
projectId: this.projectId,
|
||||
@@ -259,6 +383,7 @@ export default {
|
||||
riskModelCardsData,
|
||||
suspiciousData,
|
||||
creditNegativeData,
|
||||
externalRiskSummary,
|
||||
});
|
||||
|
||||
const hasOverviewData = Boolean(
|
||||
@@ -273,6 +398,7 @@ export default {
|
||||
this.pageState = "empty";
|
||||
this.selectedModelCodes = [];
|
||||
this.resetProjectAnalysisDialog();
|
||||
this.resetExternalDetailDialog();
|
||||
console.error("加载结果总览失败", error);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
{{ dialogData.sourceSummary.currentModelValue }}
|
||||
</span>
|
||||
</div>
|
||||
<button class="project-analysis-header__close" type="button" aria-label="关闭" @click="closeDialog">
|
||||
<i class="el-icon-close" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-loading="detailLoading" class="project-analysis-workspace">
|
||||
@@ -224,6 +227,9 @@ export default {
|
||||
this.resetDialogState();
|
||||
this.$emit("close");
|
||||
},
|
||||
closeDialog() {
|
||||
this.visibleProxy = false;
|
||||
},
|
||||
handleTabChange() {
|
||||
this.$nextTick(() => {
|
||||
const tabRef = this.activeTab === "relationshipGraph"
|
||||
@@ -303,6 +309,25 @@ export default {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.project-analysis-header__close {
|
||||
flex: 0 0 auto;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
background: transparent;
|
||||
color: #8d99aa;
|
||||
font-size: 18px;
|
||||
line-height: 34px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.project-analysis-header__close:hover {
|
||||
background: #f2f5f8;
|
||||
color: #245b8f;
|
||||
}
|
||||
|
||||
.project-analysis-workspace {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -51,7 +51,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
isFundOnly() {
|
||||
return this.initialGraphTab === "fund" && this.graphTabs.length === 2;
|
||||
return this.initialGraphTab === "fund";
|
||||
},
|
||||
rootClasses() {
|
||||
return {
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
</div>
|
||||
<div class="sidebar-profile__meta">
|
||||
<div class="sidebar-profile__item">
|
||||
<span class="sidebar-profile__label">工号</span>
|
||||
<span class="sidebar-profile__label">{{ normalizedFieldLabels.staffCode }}</span>
|
||||
<span class="sidebar-profile__value">{{ sidebarData.basicInfo.staffCode || "-" }}</span>
|
||||
</div>
|
||||
<div class="sidebar-profile__item">
|
||||
<span class="sidebar-profile__label">部门</span>
|
||||
<span class="sidebar-profile__label">{{ normalizedFieldLabels.department }}</span>
|
||||
<span class="sidebar-profile__value">{{ sidebarData.basicInfo.department || "-" }}</span>
|
||||
</div>
|
||||
<div class="sidebar-profile__item">
|
||||
<span class="sidebar-profile__label">所属项目</span>
|
||||
<span class="sidebar-profile__label">{{ normalizedFieldLabels.projectName }}</span>
|
||||
<span class="sidebar-profile__value">{{ sidebarData.basicInfo.projectName || "-" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,6 +63,23 @@ export default {
|
||||
},
|
||||
}),
|
||||
},
|
||||
fieldLabels: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
staffCode: "工号",
|
||||
department: "部门",
|
||||
projectName: "所属项目",
|
||||
}),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
normalizedFieldLabels() {
|
||||
return {
|
||||
staffCode: this.fieldLabels.staffCode || "工号",
|
||||
department: this.fieldLabels.department || "部门",
|
||||
projectName: this.fieldLabels.projectName || "所属项目",
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatRiskTag(tag) {
|
||||
|
||||
@@ -351,6 +351,7 @@ const SUSPICIOUS_TYPE_OPTIONS = [
|
||||
{ value: "ALL", label: "全部可疑人员类型" },
|
||||
{ value: "NAME_LIST", label: "名单库命中" },
|
||||
{ value: "MODEL_RULE", label: "模型规则命中" },
|
||||
{ value: "EXTERNAL_PERSON", label: "外部人员预警" },
|
||||
];
|
||||
|
||||
const normalizeSuspiciousType = (value) => (
|
||||
|
||||
@@ -8,160 +8,209 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<div class="block-header">
|
||||
<div>
|
||||
<div class="block-title">模型预警次数统计</div>
|
||||
<div class="block-subtitle">按模型汇总预警命中次数与涉及人数</div>
|
||||
</div>
|
||||
<el-button size="mini" type="text">导出</el-button>
|
||||
</div>
|
||||
|
||||
<div v-loading="cardLoading" class="model-card-grid">
|
||||
<button
|
||||
v-for="item in cards"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="model-card"
|
||||
:class="{ 'is-active': isModelSelected(item.key) }"
|
||||
@click="toggleModelSelection(item.key)"
|
||||
>
|
||||
<div class="model-card-title">{{ item.title }}</div>
|
||||
<div class="model-card-count">{{ item.count }}</div>
|
||||
<div class="model-card-meta">涉及 {{ item.peopleCount }} 人</div>
|
||||
<div class="model-card-action">
|
||||
{{ isModelSelected(item.key) ? "再次点击取消" : "点击加入联动" }}
|
||||
<div class="model-workbench">
|
||||
<section class="model-list-panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<div class="panel-title">模型预警统计</div>
|
||||
<div class="panel-subtitle">点击模型筛选下方命中人员</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<div class="block-header">
|
||||
<div>
|
||||
<div class="block-title">命中模型涉及人员</div>
|
||||
<div class="block-subtitle">基于筛选条件查看模型命中人员</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<div class="filter-item filter-item--keyword">
|
||||
<span class="filter-label">员工姓名或工号</span>
|
||||
<el-input
|
||||
v-model.trim="keyword"
|
||||
size="mini"
|
||||
clearable
|
||||
placeholder="请输入员工姓名或工号"
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
<el-button size="mini" type="text" :disabled="!projectId" @click="handleModelExport">导出</el-button>
|
||||
</div>
|
||||
|
||||
<div class="filter-item filter-item--dept">
|
||||
<span class="filter-label">部门</span>
|
||||
<el-select
|
||||
v-model="deptId"
|
||||
size="mini"
|
||||
clearable
|
||||
filterable
|
||||
:loading="deptLoading"
|
||||
placeholder="请选择部门"
|
||||
<div v-loading="cardLoading" class="model-card-grid">
|
||||
<button
|
||||
v-for="item in displayCards"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="model-card"
|
||||
:class="{ 'is-active': isModelSelected(item.key), 'is-external': item.sourceType === 'external' }"
|
||||
@click="toggleModelSelection(item.key)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in deptOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<span class="model-card-head">
|
||||
<span v-if="item.sourceType === 'external'" class="source-pill is-external">
|
||||
{{ formatSourceLabel(item) }}
|
||||
</span>
|
||||
<span class="model-card-action">{{ isModelSelected(item.key) ? "已筛选" : "筛选" }}</span>
|
||||
</span>
|
||||
<span class="model-card-title">{{ item.title }}</span>
|
||||
<span class="model-card-metrics">
|
||||
<span><strong>{{ item.count }}</strong><em>次</em></span>
|
||||
<span><strong>{{ item.peopleCount }}</strong><em>人</em></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="model-result-panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<div class="panel-title">命中模型涉及人员</div>
|
||||
<div class="panel-subtitle">按模型、人员信息和触发方式查看明细</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-item filter-item--mode">
|
||||
<span class="filter-label">触发方式</span>
|
||||
<el-radio-group
|
||||
v-model="matchMode"
|
||||
size="mini"
|
||||
@change="handleMatchModeChange"
|
||||
>
|
||||
<el-radio-button label="ANY">任意触发</el-radio-button>
|
||||
<el-radio-button label="ALL">同时触发</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="filter-summary">
|
||||
<span class="summary-label">已选模型</span>
|
||||
<span class="summary-value">{{ selectedModelText }}</span>
|
||||
</div>
|
||||
|
||||
<div class="filter-actions">
|
||||
<el-button size="mini" type="primary" @click="handleQuery">查询</el-button>
|
||||
<el-button size="mini" plain @click="resetQuery">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="tableLoading" :data="peopleList" class="model-table">
|
||||
<template slot="empty">
|
||||
<el-empty :image-size="80" description="当前筛选条件下暂无命中人员" />
|
||||
</template>
|
||||
<el-table-column type="index" label="序号" width="60" />
|
||||
<el-table-column prop="name" label="姓名" min-width="100" />
|
||||
<el-table-column prop="staffCode" label="工号" min-width="120" />
|
||||
<el-table-column prop="idNo" label="身份证号" min-width="180" />
|
||||
<el-table-column prop="department" label="所属部门" min-width="140" />
|
||||
<el-table-column label="命中模型" min-width="180">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatModelNames(scope.row.modelNames) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="异常标签" min-width="220">
|
||||
<template slot-scope="scope">
|
||||
<div v-if="scope.row.hitTagList && scope.row.hitTagList.length" class="hit-tag-list">
|
||||
<div class="selected-strip">
|
||||
<span class="selected-label">当前筛选</span>
|
||||
<div class="selected-content">
|
||||
<template v-if="selectedCards.length">
|
||||
<el-tag
|
||||
v-for="item in selectedCards"
|
||||
:key="`selected-${item.key}`"
|
||||
size="mini"
|
||||
closable
|
||||
effect="plain"
|
||||
@close="toggleModelSelection(item.key)"
|
||||
>
|
||||
{{ item.title }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<span v-else class="selected-placeholder">全部模型</span>
|
||||
</div>
|
||||
<el-button v-if="selectedCards.length" size="mini" type="text" @click="clearSelectedModels">
|
||||
清空模型
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<div class="filter-item filter-item--keyword">
|
||||
<span class="filter-label">{{ keywordLabel }}</span>
|
||||
<el-input
|
||||
v-model.trim="keyword"
|
||||
size="mini"
|
||||
clearable
|
||||
:placeholder="keywordPlaceholder"
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="activeSourceType === 'employee'" class="filter-item filter-item--dept">
|
||||
<span class="filter-label">部门</span>
|
||||
<el-select
|
||||
v-model="deptId"
|
||||
size="mini"
|
||||
clearable
|
||||
filterable
|
||||
:loading="deptLoading"
|
||||
placeholder="请选择部门"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in deptOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="filter-item filter-item--mode">
|
||||
<span class="filter-label">模型关系</span>
|
||||
<el-radio-group
|
||||
v-model="matchMode"
|
||||
size="mini"
|
||||
@change="handleMatchModeChange"
|
||||
>
|
||||
<el-radio-button label="ANY">命中任一模型</el-radio-button>
|
||||
<el-radio-button label="ALL">同时命中全部</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="filter-actions">
|
||||
<el-button size="mini" type="primary" @click="handleQuery">查询</el-button>
|
||||
<el-button size="mini" plain @click="resetQuery">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="tableLoading" :data="peopleList" class="model-table">
|
||||
<template slot="empty">
|
||||
<el-empty :image-size="80" description="当前筛选条件下暂无命中人员" />
|
||||
</template>
|
||||
<el-table-column type="index" label="序号" width="60" />
|
||||
<el-table-column v-if="activeSourceType === 'all'" label="来源" width="84">
|
||||
<template slot-scope="scope">
|
||||
<el-tag
|
||||
v-for="(tag, index) in scope.row.hitTagList"
|
||||
:key="`${scope.row.staffCode || scope.row.idNo || index}-tag-${index}`"
|
||||
size="mini"
|
||||
effect="plain"
|
||||
:type="resolveModelTagType(tag)"
|
||||
:type="scope.row.sourceType === 'external' ? 'success' : ''"
|
||||
>
|
||||
{{ tag.ruleName }}
|
||||
{{ scope.row.sourceType === "external" ? "外部" : "员工" }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<span v-else class="empty-text">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" align="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="handleViewProject(scope.row)">{{
|
||||
scope.row.actionLabel || "查看项目"
|
||||
}}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="姓名" min-width="100" />
|
||||
<el-table-column
|
||||
v-if="activeSourceType !== 'external'"
|
||||
prop="staffCode"
|
||||
:label="activeSourceType === 'all' ? '工号/主体类型' : '工号'"
|
||||
min-width="120"
|
||||
/>
|
||||
<el-table-column prop="idNo" label="身份证号" min-width="180" />
|
||||
<el-table-column
|
||||
prop="department"
|
||||
:label="departmentLabel"
|
||||
min-width="140"
|
||||
/>
|
||||
<el-table-column label="命中模型" min-width="180">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatModelNames(scope.row.modelNames) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="异常标签" min-width="220">
|
||||
<template slot-scope="scope">
|
||||
<div v-if="scope.row.hitTagList && scope.row.hitTagList.length" class="hit-tag-list">
|
||||
<el-tag
|
||||
v-for="(tag, index) in scope.row.hitTagList"
|
||||
:key="`${scope.row.staffCode || scope.row.idNo || index}-tag-${index}`"
|
||||
size="mini"
|
||||
effect="plain"
|
||||
:type="resolveModelTagType(tag)"
|
||||
>
|
||||
{{ tag.ruleName }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<span v-else class="empty-text">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" align="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="handleViewProject(scope.row)">{{
|
||||
scope.row.actionLabel || "查看项目"
|
||||
}}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:current-page="pageNum"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:current-page="pageNum"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getOverviewRiskModelPeople } from "@/api/ccdi/projectOverview";
|
||||
import {
|
||||
getOverviewExternalRiskModelCards,
|
||||
getOverviewExternalRiskModelPeople,
|
||||
getOverviewRiskModelPeople,
|
||||
} from "@/api/ccdi/projectOverview";
|
||||
import { deptTreeSelect } from "@/api/system/user";
|
||||
|
||||
function normalizePeopleRows(rows) {
|
||||
function normalizePeopleRows(rows, sourceType) {
|
||||
if (!Array.isArray(rows)) {
|
||||
return [];
|
||||
}
|
||||
return rows.map((item) => ({
|
||||
...item,
|
||||
sourceType,
|
||||
name: item.staffName || item.name || "",
|
||||
staffCode: item.staffCode || "",
|
||||
modelNames: Array.isArray(item.modelNames) ? item.modelNames : [],
|
||||
@@ -206,6 +255,7 @@ export default {
|
||||
tableLoading: false,
|
||||
deptLoading: false,
|
||||
deptOptions: [],
|
||||
externalCards: [],
|
||||
peopleList: [],
|
||||
total: 0,
|
||||
};
|
||||
@@ -218,22 +268,65 @@ export default {
|
||||
if (!Array.isArray(this.sectionData && this.sectionData.cardList)) {
|
||||
return [];
|
||||
}
|
||||
return this.sectionData.cardList.map((item) => ({
|
||||
key: item.key || item.modelCode,
|
||||
title: item.title || item.modelName,
|
||||
count: item.count || item.warningCount || 0,
|
||||
peopleCount: item.peopleCount || 0,
|
||||
}));
|
||||
return this.sectionData.cardList
|
||||
.filter((item) => !String(item.key || item.modelCode || "").startsWith("EXTERNAL_"))
|
||||
.map((item) => ({
|
||||
key: item.key || item.modelCode,
|
||||
title: item.title || item.modelName,
|
||||
count: item.count || item.warningCount || 0,
|
||||
peopleCount: item.peopleCount || 0,
|
||||
sourceType: "employee",
|
||||
}));
|
||||
},
|
||||
displayCards() {
|
||||
return [...this.cards, ...this.externalCards];
|
||||
},
|
||||
activeSourceType() {
|
||||
if (!this.selectedModelCodes.length) {
|
||||
return "all";
|
||||
}
|
||||
const selected = this.displayCards.find((item) => this.selectedModelCodes.includes(item.key));
|
||||
return selected && selected.sourceType === "external" ? "external" : "employee";
|
||||
},
|
||||
keywordLabel() {
|
||||
if (this.activeSourceType === "external") {
|
||||
return "姓名或证件号";
|
||||
}
|
||||
if (this.activeSourceType === "all") {
|
||||
return "姓名/证件号/工号";
|
||||
}
|
||||
return "员工姓名或工号";
|
||||
},
|
||||
keywordPlaceholder() {
|
||||
if (this.activeSourceType === "external") {
|
||||
return "请输入姓名或证件号";
|
||||
}
|
||||
if (this.activeSourceType === "all") {
|
||||
return "请输入姓名、证件号或工号";
|
||||
}
|
||||
return "请输入员工姓名或工号";
|
||||
},
|
||||
departmentLabel() {
|
||||
if (this.activeSourceType === "external") {
|
||||
return "涉及对象";
|
||||
}
|
||||
if (this.activeSourceType === "all") {
|
||||
return "所属部门/涉及对象";
|
||||
}
|
||||
return "所属部门";
|
||||
},
|
||||
selectedModelText() {
|
||||
if (!this.selectedModelCodes.length) {
|
||||
return "全部模型";
|
||||
}
|
||||
return this.cards
|
||||
return this.displayCards
|
||||
.filter((item) => this.selectedModelCodes.includes(item.key))
|
||||
.map((item) => item.title)
|
||||
.join("、");
|
||||
},
|
||||
selectedCards() {
|
||||
return this.displayCards.filter((item) => this.selectedModelCodes.includes(item.key));
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
projectId: {
|
||||
@@ -242,6 +335,7 @@ export default {
|
||||
this.selectedModelCodes = [];
|
||||
this.$emit("selection-change", this.selectedModelCodes);
|
||||
this.pageNum = 1;
|
||||
this.loadExternalCards();
|
||||
this.fetchPeopleList();
|
||||
},
|
||||
},
|
||||
@@ -253,13 +347,31 @@ export default {
|
||||
isModelSelected(modelCode) {
|
||||
return this.selectedModelCodes.includes(modelCode);
|
||||
},
|
||||
formatSourceLabel(item) {
|
||||
return item && item.sourceType === "external" ? "外部" : "员工";
|
||||
},
|
||||
clearSelectedModels() {
|
||||
this.selectedModelCodes = [];
|
||||
this.$emit("selection-change", this.selectedModelCodes);
|
||||
this.pageNum = 1;
|
||||
this.fetchPeopleList({ syncCardLoading: true });
|
||||
},
|
||||
toggleModelSelection(modelCode) {
|
||||
const target = this.displayCards.find((item) => item.key === modelCode);
|
||||
const targetSourceType = target && target.sourceType === "external" ? "external" : "employee";
|
||||
const currentSourceType = this.activeSourceType;
|
||||
if (this.selectedModelCodes.length && targetSourceType !== currentSourceType) {
|
||||
this.selectedModelCodes = [];
|
||||
}
|
||||
if (this.selectedModelCodes.includes(modelCode)) {
|
||||
this.selectedModelCodes = this.selectedModelCodes.filter((item) => item !== modelCode);
|
||||
} else {
|
||||
this.selectedModelCodes = [...this.selectedModelCodes, modelCode];
|
||||
}
|
||||
this.$emit("selection-change", this.selectedModelCodes);
|
||||
if (targetSourceType === "external") {
|
||||
this.deptId = undefined;
|
||||
}
|
||||
this.pageNum = 1;
|
||||
this.fetchPeopleList({ syncCardLoading: true });
|
||||
},
|
||||
@@ -284,15 +396,16 @@ export default {
|
||||
this.pageNum = pageNum;
|
||||
this.fetchPeopleList();
|
||||
},
|
||||
buildPeopleParams() {
|
||||
buildPeopleParams(sourceType = this.activeSourceType, overrides = {}) {
|
||||
return {
|
||||
projectId: this.projectId,
|
||||
modelCodes: this.selectedModelCodes,
|
||||
matchMode: this.matchMode,
|
||||
keyword: this.keyword,
|
||||
deptId: this.deptId,
|
||||
deptId: sourceType === "employee" && this.activeSourceType === "employee" ? this.deptId : undefined,
|
||||
pageNum: this.pageNum,
|
||||
pageSize: this.pageSize,
|
||||
...overrides,
|
||||
};
|
||||
},
|
||||
formatModelNames(modelNames) {
|
||||
@@ -302,6 +415,10 @@ export default {
|
||||
return modelNames.join("、");
|
||||
},
|
||||
handleViewProject(row) {
|
||||
if ((row && row.sourceType === "external") || this.activeSourceType === "external") {
|
||||
this.$emit("view-external-detail", row);
|
||||
return;
|
||||
}
|
||||
this.$emit("view-project-analysis", row);
|
||||
},
|
||||
resolveModelTagType(tag) {
|
||||
@@ -325,6 +442,44 @@ export default {
|
||||
this.deptLoading = false;
|
||||
}
|
||||
},
|
||||
async loadExternalCards() {
|
||||
if (!this.projectId) {
|
||||
this.externalCards = [];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await getOverviewExternalRiskModelCards(this.projectId);
|
||||
const rows = Array.isArray(response && response.data && response.data.cardList)
|
||||
? response.data.cardList
|
||||
: [];
|
||||
this.externalCards = rows.map((item) => ({
|
||||
key: item.modelCode,
|
||||
title: item.modelName,
|
||||
count: item.warningCount || 0,
|
||||
peopleCount: item.peopleCount || 0,
|
||||
sourceType: "external",
|
||||
}));
|
||||
} catch (error) {
|
||||
this.externalCards = [];
|
||||
console.error("加载外部人员风险模型失败", error);
|
||||
}
|
||||
},
|
||||
handleModelExport() {
|
||||
if (!this.projectId) {
|
||||
return;
|
||||
}
|
||||
const url = this.activeSourceType === "external"
|
||||
? "ccdi/project/overview/external-risk-models/people/export"
|
||||
: "ccdi/project/overview/risk-models/people/export";
|
||||
const filePrefix = this.activeSourceType === "external"
|
||||
? "外部人员风险模型命中明细"
|
||||
: "风险模型命中明细";
|
||||
this.download(
|
||||
url,
|
||||
this.buildPeopleParams(),
|
||||
`${filePrefix}_${this.projectId}_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
},
|
||||
async fetchPeopleList(options = {}) {
|
||||
if (!this.projectId) {
|
||||
this.peopleList = [];
|
||||
@@ -338,9 +493,19 @@ export default {
|
||||
}
|
||||
this.tableLoading = true;
|
||||
try {
|
||||
const response = await getOverviewRiskModelPeople(this.buildPeopleParams());
|
||||
if (this.activeSourceType === "all") {
|
||||
await this.fetchAllPeopleList();
|
||||
return;
|
||||
}
|
||||
const requestFn = this.activeSourceType === "external"
|
||||
? getOverviewExternalRiskModelPeople
|
||||
: getOverviewRiskModelPeople;
|
||||
const response = await requestFn(this.buildPeopleParams());
|
||||
const data = (response && response.data) || {};
|
||||
this.peopleList = normalizePeopleRows(data.rows);
|
||||
this.peopleList = normalizePeopleRows(
|
||||
data.rows,
|
||||
this.activeSourceType === "external" ? "external" : "employee"
|
||||
);
|
||||
this.total = Number(data.total || 0);
|
||||
} catch (error) {
|
||||
this.peopleList = [];
|
||||
@@ -353,6 +518,55 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
async fetchAllPeopleList() {
|
||||
const countParams = {
|
||||
modelCodes: [],
|
||||
pageNum: 1,
|
||||
pageSize: 1,
|
||||
};
|
||||
const [employeeCountRes, externalCountRes] = await Promise.all([
|
||||
getOverviewRiskModelPeople(this.buildPeopleParams("employee", countParams)),
|
||||
getOverviewExternalRiskModelPeople(this.buildPeopleParams("external", countParams)),
|
||||
]);
|
||||
const employeeTotal = Number((employeeCountRes && employeeCountRes.data && employeeCountRes.data.total) || 0);
|
||||
const externalTotal = Number((externalCountRes && externalCountRes.data && externalCountRes.data.total) || 0);
|
||||
const offset = (this.pageNum - 1) * this.pageSize;
|
||||
const rows = [];
|
||||
|
||||
if (offset < employeeTotal) {
|
||||
const employeeLimit = offset + this.pageSize;
|
||||
const employeeRes = await getOverviewRiskModelPeople(this.buildPeopleParams("employee", {
|
||||
modelCodes: [],
|
||||
pageNum: 1,
|
||||
pageSize: employeeLimit,
|
||||
}));
|
||||
const employeeRows = normalizePeopleRows(
|
||||
employeeRes && employeeRes.data && employeeRes.data.rows,
|
||||
"employee"
|
||||
);
|
||||
rows.push(...employeeRows.slice(offset, offset + this.pageSize));
|
||||
}
|
||||
|
||||
if (rows.length < this.pageSize) {
|
||||
const externalOffset = Math.max(0, offset - employeeTotal);
|
||||
if (externalOffset < externalTotal) {
|
||||
const externalLimit = externalOffset + (this.pageSize - rows.length);
|
||||
const externalRes = await getOverviewExternalRiskModelPeople(this.buildPeopleParams("external", {
|
||||
modelCodes: [],
|
||||
pageNum: 1,
|
||||
pageSize: externalLimit,
|
||||
}));
|
||||
const externalRows = normalizePeopleRows(
|
||||
externalRes && externalRes.data && externalRes.data.rows,
|
||||
"external"
|
||||
);
|
||||
rows.push(...externalRows.slice(externalOffset, externalOffset + (this.pageSize - rows.length)));
|
||||
}
|
||||
}
|
||||
|
||||
this.peopleList = rows;
|
||||
this.total = employeeTotal + externalTotal;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -363,8 +577,8 @@ export default {
|
||||
}
|
||||
|
||||
.section-card {
|
||||
padding: 20px;
|
||||
border-radius: 14px;
|
||||
padding: 18px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--ccdi-border);
|
||||
box-shadow: var(--ccdi-shadow);
|
||||
@@ -389,40 +603,44 @@ export default {
|
||||
color: var(--ccdi-text-muted);
|
||||
}
|
||||
|
||||
.block + .block {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.block-header {
|
||||
.model-workbench {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.block-title {
|
||||
position: relative;
|
||||
padding-left: 10px;
|
||||
.model-list-panel,
|
||||
.model-result-panel {
|
||||
border: 1px solid var(--ccdi-border);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.model-result-panel {
|
||||
min-width: 0;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 54px;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid #e7eef5;
|
||||
background: #f8fbfe;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--ccdi-text-primary);
|
||||
}
|
||||
|
||||
.block-title::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
width: 4px;
|
||||
height: 14px;
|
||||
border-radius: 999px;
|
||||
background: #8ea7be;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.block-subtitle {
|
||||
margin-top: 4px;
|
||||
padding-left: 10px;
|
||||
.panel-subtitle {
|
||||
margin-top: 3px;
|
||||
font-size: 12px;
|
||||
color: var(--ccdi-text-muted);
|
||||
}
|
||||
@@ -430,62 +648,159 @@ export default {
|
||||
.model-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.model-card {
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-height: 104px;
|
||||
width: 100%;
|
||||
padding: 18px;
|
||||
border: 1px solid var(--ccdi-border);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fbfd 100%);
|
||||
padding: 12px;
|
||||
border: 1px solid #e4edf6;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: var(--ccdi-text-primary);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||
transition: border-color 0.16s ease, background 0.16s ease, box-shadow 0.16s ease;
|
||||
}
|
||||
|
||||
.model-card:hover,
|
||||
.model-card.is-active {
|
||||
border-color: #bdd0e2;
|
||||
box-shadow: 0 10px 22px rgba(47, 93, 138, 0.11);
|
||||
transform: translateY(-2px);
|
||||
background: #f3f8fd;
|
||||
}
|
||||
|
||||
.model-card.is-active {
|
||||
box-shadow: inset 0 3px 0 var(--ccdi-primary);
|
||||
}
|
||||
|
||||
.model-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.source-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 20px;
|
||||
border-radius: 999px;
|
||||
background: #edf2f7;
|
||||
color: #52677d;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.source-pill.is-external {
|
||||
background: #edf7ff;
|
||||
color: #2f5f91;
|
||||
}
|
||||
|
||||
.model-card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--ccdi-text-primary);
|
||||
}
|
||||
|
||||
.model-card-count {
|
||||
display: block;
|
||||
margin-top: 12px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--ccdi-primary);
|
||||
color: var(--ccdi-text-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.model-card-meta {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
.model-card-metrics {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.model-card-metrics span {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
color: var(--ccdi-text-muted);
|
||||
}
|
||||
|
||||
.model-card-action {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
.model-card-metrics strong {
|
||||
color: var(--ccdi-primary);
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.model-card-metrics em {
|
||||
color: var(--ccdi-text-muted);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.model-card-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 22px;
|
||||
min-width: 44px;
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
color: var(--ccdi-primary);
|
||||
font-size: 12px;
|
||||
background: #eef6ff;
|
||||
}
|
||||
|
||||
.model-card:not(.is-active) .model-card-action {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.model-card.is-active .model-card-action {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.selected-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 12px 14px 0;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #e6edf5;
|
||||
border-radius: 6px;
|
||||
background: #fbfdff;
|
||||
}
|
||||
|
||||
.selected-label {
|
||||
color: var(--ccdi-text-secondary);
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.selected-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.selected-placeholder {
|
||||
color: var(--ccdi-text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 14px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--ccdi-border);
|
||||
border-radius: 12px;
|
||||
background: #f8fbfe;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 14px;
|
||||
margin: 10px 14px 12px;
|
||||
padding: 10px;
|
||||
border: 1px solid #e6edf5;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
@@ -500,12 +815,12 @@ export default {
|
||||
|
||||
.filter-item--keyword,
|
||||
.filter-item--dept {
|
||||
min-width: 240px;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.filter-item--keyword :deep(.el-input),
|
||||
.filter-item--dept :deep(.el-select) {
|
||||
width: 220px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.filter-label,
|
||||
@@ -516,23 +831,6 @@ export default {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
min-width: 0;
|
||||
color: var(--ccdi-text-primary);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
@@ -540,7 +838,10 @@ export default {
|
||||
}
|
||||
|
||||
.model-table {
|
||||
border-radius: 12px;
|
||||
margin: 0 14px;
|
||||
width: calc(100% - 28px);
|
||||
border: 1px solid #e7eef5;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -562,12 +863,12 @@ export default {
|
||||
.pagination-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
margin: 12px 14px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.model-card-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -575,5 +876,17 @@ export default {
|
||||
.model-card-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.filter-item--keyword,
|
||||
.filter-item--dept,
|
||||
.filter-item--keyword :deep(.el-input),
|
||||
.filter-item--dept :deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
<template>
|
||||
<section class="risk-people-section">
|
||||
<div class="section-toolbar">
|
||||
<el-button size="mini" type="text" @click="handleRiskPeopleExport">导出</el-button>
|
||||
<el-tabs v-model="activeTab" class="people-tabs" @tab-click="handleTabChange">
|
||||
<el-tab-pane label="员工风险人员" name="employee" />
|
||||
<el-tab-pane v-if="hasExternalWarnings" :label="externalTabLabel" name="external" />
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="tableLoading" :data="overviewList" class="people-table">
|
||||
<el-table
|
||||
v-if="activeTab === 'employee'"
|
||||
v-loading="tableLoading"
|
||||
:data="overviewList"
|
||||
class="people-table"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" />
|
||||
<el-table-column prop="name" label="姓名" min-width="100" />
|
||||
<el-table-column prop="idNo" label="身份证号" min-width="180" />
|
||||
@@ -46,11 +54,69 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-table
|
||||
v-else
|
||||
v-loading="externalLoading"
|
||||
:data="externalRows"
|
||||
class="people-table"
|
||||
>
|
||||
<template slot="empty">
|
||||
<el-empty :image-size="80" description="当前项目暂无外部人员预警" />
|
||||
</template>
|
||||
<el-table-column type="index" label="序号" width="60" />
|
||||
<el-table-column prop="name" label="姓名" min-width="100" />
|
||||
<el-table-column prop="idNo" label="证件号" min-width="180" />
|
||||
<el-table-column prop="subjectType" label="主体类型" min-width="110">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" effect="plain">{{ scope.row.subjectType || "外部人员" }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="riskLevel" label="风险等级" min-width="110">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="scope.row.riskLevelType" effect="plain">
|
||||
{{ scope.row.riskLevel }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="核心异常点" min-width="260">
|
||||
<template slot-scope="scope">
|
||||
<div
|
||||
v-if="scope.row.riskPointTagList && scope.row.riskPointTagList.length"
|
||||
class="risk-point-tag-list"
|
||||
>
|
||||
<el-tag
|
||||
v-for="(tag, index) in scope.row.riskPointTagList"
|
||||
:key="`${scope.row.idNo || scope.row.name || index}-external-risk-point-${index}`"
|
||||
class="core-risk-tag"
|
||||
:style="resolveModelTagStyle(tag)"
|
||||
size="mini"
|
||||
effect="plain"
|
||||
>
|
||||
{{ tag.ruleName }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<span v-else class="empty-text">{{ scope.row.riskPoint || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="relatedObject" label="涉及对象" min-width="140" />
|
||||
<el-table-column label="最近交易时间" min-width="170">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatLatestTradeTime(scope.row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" align="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="handleViewExternal(scope.row)">
|
||||
查看详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="pageNum"
|
||||
:limit.sync="pageSize"
|
||||
v-show="currentTotal > 0"
|
||||
:total="currentTotal"
|
||||
:page.sync="currentPageNum"
|
||||
:limit.sync="currentPageSize"
|
||||
:page-sizes="[5]"
|
||||
layout="total, prev, pager, next, jumper"
|
||||
@pagination="handlePageChange"
|
||||
@@ -59,7 +125,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getOverviewRiskPeople } from "@/api/ccdi/projectOverview";
|
||||
import {
|
||||
getOverviewExternalPersons,
|
||||
getOverviewRiskPeople,
|
||||
} from "@/api/ccdi/projectOverview";
|
||||
|
||||
// 历史静态回归锚点:scope.row.actionLabel || "查看详情"
|
||||
const CORE_TAG_PALETTE = {
|
||||
@@ -157,10 +226,13 @@ function normalizeOverviewRows(rows) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return rows.map((item) => ({
|
||||
...item,
|
||||
riskPointTagList: normalizeRiskPointTags(item.riskPointTagList, item.riskPoint, item.riskLevelType),
|
||||
}));
|
||||
return rows.map((item) => {
|
||||
const riskPointTagList = normalizeRiskPointTags(item.riskPointTagList, item.riskPoint, item.riskLevelType);
|
||||
return {
|
||||
...item,
|
||||
riskPointTagList,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -182,12 +254,51 @@ export default {
|
||||
total: 0,
|
||||
tableLoading: false,
|
||||
localRows: [],
|
||||
activeTab: "employee",
|
||||
externalRows: [],
|
||||
externalTotal: 0,
|
||||
externalPageNum: 1,
|
||||
externalPageSize: 5,
|
||||
externalLoading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
overviewList() {
|
||||
return this.localRows;
|
||||
},
|
||||
externalTabLabel() {
|
||||
return `外部人员预警(${this.externalTotal})`;
|
||||
},
|
||||
hasExternalWarnings() {
|
||||
return this.externalTotal > 0;
|
||||
},
|
||||
currentTotal() {
|
||||
return this.activeTab === "external" ? this.externalTotal : this.total;
|
||||
},
|
||||
currentPageNum: {
|
||||
get() {
|
||||
return this.activeTab === "external" ? this.externalPageNum : this.pageNum;
|
||||
},
|
||||
set(value) {
|
||||
if (this.activeTab === "external") {
|
||||
this.externalPageNum = value;
|
||||
return;
|
||||
}
|
||||
this.pageNum = value;
|
||||
},
|
||||
},
|
||||
currentPageSize: {
|
||||
get() {
|
||||
return this.activeTab === "external" ? this.externalPageSize : this.pageSize;
|
||||
},
|
||||
set(value) {
|
||||
if (this.activeTab === "external") {
|
||||
this.externalPageSize = value;
|
||||
return;
|
||||
}
|
||||
this.pageSize = value;
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
sectionData: {
|
||||
@@ -198,6 +309,7 @@ export default {
|
||||
this.total = (this.sectionData && this.sectionData.total) || 0;
|
||||
this.pageNum = (this.sectionData && this.sectionData.pageNum) || 1;
|
||||
this.pageSize = (this.sectionData && this.sectionData.pageSize) || 5;
|
||||
this.loadExternalPage(1);
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -205,22 +317,33 @@ export default {
|
||||
resolveModelTagStyle(tag) {
|
||||
return CORE_TAG_PALETTE[tag.modelCode] || {};
|
||||
},
|
||||
handleRiskPeopleExport() {
|
||||
if (!this.projectId) {
|
||||
return;
|
||||
}
|
||||
this.download(
|
||||
"ccdi/project/overview/risk-people/export",
|
||||
{
|
||||
projectId: this.projectId,
|
||||
},
|
||||
`风险人员总览_${this.projectId}_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
},
|
||||
handleViewProject(row) {
|
||||
this.$emit("view-project-analysis", row);
|
||||
},
|
||||
handleViewExternal(row) {
|
||||
this.$emit("view-external-detail", row);
|
||||
},
|
||||
formatLatestTradeTime(row) {
|
||||
const value = row && row.latestTradeTime ? String(row.latestTradeTime) : "";
|
||||
if (!value || value === row.actionLabel) {
|
||||
return "-";
|
||||
}
|
||||
return value;
|
||||
},
|
||||
handleTabChange() {
|
||||
if (this.activeTab === "external" && !this.hasExternalWarnings) {
|
||||
this.activeTab = "employee";
|
||||
}
|
||||
this.emitScopeSummary();
|
||||
if (this.activeTab === "external" && !this.externalRows.length) {
|
||||
this.loadExternalPage(1);
|
||||
}
|
||||
},
|
||||
handlePageChange({ page }) {
|
||||
if (this.activeTab === "external") {
|
||||
this.loadExternalPage(page);
|
||||
return;
|
||||
}
|
||||
this.loadRiskPeoplePage(page);
|
||||
},
|
||||
async loadRiskPeoplePage(pageNum) {
|
||||
@@ -243,6 +366,44 @@ export default {
|
||||
this.tableLoading = false;
|
||||
}
|
||||
},
|
||||
async loadExternalPage(pageNum) {
|
||||
if (!this.projectId) {
|
||||
this.externalRows = [];
|
||||
this.externalTotal = 0;
|
||||
return;
|
||||
}
|
||||
this.externalLoading = true;
|
||||
try {
|
||||
const response = await getOverviewExternalPersons({
|
||||
projectId: this.projectId,
|
||||
pageNum,
|
||||
pageSize: 5,
|
||||
});
|
||||
const data = (response && response.data) || {};
|
||||
this.externalRows = normalizeOverviewRows(data.rows);
|
||||
this.externalTotal = data.total || 0;
|
||||
this.externalPageNum = data.pageNum || pageNum;
|
||||
this.externalPageSize = data.pageSize || 5;
|
||||
if (!this.hasExternalWarnings && this.activeTab === "external") {
|
||||
this.activeTab = "employee";
|
||||
}
|
||||
this.emitScopeSummary();
|
||||
} finally {
|
||||
this.externalLoading = false;
|
||||
}
|
||||
},
|
||||
emitScopeSummary() {
|
||||
this.$emit("scope-summary-change", {
|
||||
activeTab: this.activeTab,
|
||||
employee: {
|
||||
total: this.total,
|
||||
},
|
||||
external: {
|
||||
total: this.externalTotal,
|
||||
rows: this.externalRows,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -256,11 +417,19 @@ export default {
|
||||
|
||||
.section-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.people-tabs {
|
||||
flex: 1;
|
||||
|
||||
:deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.people-table {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -8,7 +8,7 @@ export const mockOverviewData = {
|
||||
{ key: "riskPeople", label: "高风险", value: 10, icon: "el-icon-warning-outline", tone: "red" },
|
||||
{ key: "medium", label: "中风险", value: 20, icon: "el-icon-s-opportunity", tone: "amber" },
|
||||
{ key: "low", label: "低风险", value: 38, icon: "el-icon-data-line", tone: "green" },
|
||||
{ key: "count", label: "无预警人数", value: 432, icon: "el-icon-document", tone: "blue" },
|
||||
{ key: "count", label: "无风险", value: 432, icon: "el-icon-s-data", tone: "blue" },
|
||||
],
|
||||
},
|
||||
riskPeople: {
|
||||
@@ -433,6 +433,7 @@ export function createOverviewLoadedData({
|
||||
riskModelCardsData,
|
||||
suspiciousData,
|
||||
creditNegativeData,
|
||||
externalRiskSummary,
|
||||
} = {}) {
|
||||
return {
|
||||
...mockOverviewData,
|
||||
@@ -441,6 +442,7 @@ export function createOverviewLoadedData({
|
||||
...(dashboardData || {}),
|
||||
actions: mockOverviewData.summary.actions,
|
||||
stats: normalizeSummaryStats(dashboardData && dashboardData.stats),
|
||||
externalRiskSummary: externalRiskSummary || {},
|
||||
},
|
||||
riskPeople: {
|
||||
...mockOverviewData.riskPeople,
|
||||
|
||||
@@ -69,6 +69,8 @@
|
||||
@generate-report="handleGenerateReport"
|
||||
@fetch-bank-info="handleFetchBankInfo"
|
||||
@evidence-confirm="handleEvidenceConfirm"
|
||||
@open-detail-query="handleOpenDetailQuery"
|
||||
:detail-query-prefill="detailQueryPrefill"
|
||||
/>
|
||||
|
||||
<evidence-confirm-dialog
|
||||
@@ -130,6 +132,7 @@ export default {
|
||||
evidenceConfirmVisible: false,
|
||||
evidenceDrawerVisible: false,
|
||||
evidencePayload: {},
|
||||
detailQueryPrefill: null,
|
||||
projectStatusPollingTimer: null,
|
||||
projectStatusPollingInterval: 1000,
|
||||
projectStatusPollingLoading: false,
|
||||
@@ -397,6 +400,14 @@ export default {
|
||||
// 直接触发菜单选择
|
||||
this.handleMenuSelect(route);
|
||||
},
|
||||
handleOpenDetailQuery(payload = {}) {
|
||||
this.detailQueryPrefill = {
|
||||
...payload,
|
||||
nonce: Date.now(),
|
||||
};
|
||||
this.setActiveTab("detail");
|
||||
this.syncRouteTab("detail");
|
||||
},
|
||||
/** UploadData 组件:数据上传完成 */
|
||||
handleDataUploaded({ type }) {
|
||||
console.log("数据上传完成:", type);
|
||||
|
||||
@@ -33,6 +33,7 @@ module.exports = {
|
||||
host: '0.0.0.0',
|
||||
port: port,
|
||||
open: true,
|
||||
historyApiFallback: true,
|
||||
proxy: {
|
||||
// detail: https://cli.vuejs.org/config/#devserver-proxy
|
||||
[process.env.VUE_APP_BASE_API]: {
|
||||
|
||||
Reference in New Issue
Block a user