优化项目分析详情与异常明细展示

This commit is contained in:
wjj
2026-07-01 11:26:01 +08:00
parent 066d850389
commit 04ede64767
17 changed files with 724 additions and 131 deletions

View File

@@ -17,4 +17,6 @@ public class CcdiProjectRiskHitTagVO {
private String ruleName; private String ruleName;
private String riskLevel; private String riskLevel;
private String reasonDetail;
} }

View File

@@ -631,7 +631,8 @@
tr.model_name, tr.model_name,
tr.rule_code, tr.rule_code,
tr.rule_name, tr.rule_name,
tr.risk_level tr.risk_level,
tr.reason_detail
from ( from (
<include refid="externalPersonSubjectSql"/> <include refid="externalPersonSubjectSql"/>
) subject ) subject
@@ -678,7 +679,8 @@
tr.model_name, tr.model_name,
tr.rule_code, tr.rule_code,
tr.rule_name, tr.rule_name,
tr.risk_level tr.risk_level,
tr.reason_detail
from ( from (
<include refid="externalPersonSubjectSql"/> <include refid="externalPersonSubjectSql"/>
) subject ) subject
@@ -912,12 +914,16 @@
<select id="selectExternalRiskHitTagsByScope" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO"> <select id="selectExternalRiskHitTagsByScope" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO">
<bind name="externalProjectId" value="projectId"/> <bind name="externalProjectId" value="projectId"/>
select distinct select
source.model_code as modelCode, source.model_code as modelCode,
source.model_name as modelName, max(source.model_name) as modelName,
source.rule_code as ruleCode, source.rule_code as ruleCode,
source.rule_name as ruleName, max(source.rule_name) as ruleName,
source.risk_level as riskLevel max(source.risk_level) as riskLevel,
coalesce(
max(case when source.bank_statement_id is null then nullif(source.reason_detail, '') end),
max(nullif(source.reason_detail, ''))
) as reasonDetail
from ( from (
<include refid="externalPersonSourceSql"/> <include refid="externalPersonSourceSql"/>
) source ) source
@@ -925,6 +931,7 @@
<if test="selectedModelCodes != null and selectedModelCodes != ''"> <if test="selectedModelCodes != null and selectedModelCodes != ''">
and find_in_set(source.model_code, #{selectedModelCodes}) and find_in_set(source.model_code, #{selectedModelCodes})
</if> </if>
group by source.model_code, source.rule_code
order by source.model_code asc, source.rule_code asc order by source.model_code asc, source.rule_code asc
</select> </select>
@@ -1437,7 +1444,8 @@
max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].modelName')))) as model_name, max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].modelName')))) as model_name,
json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].ruleCode'))) as rule_code, json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].ruleCode'))) as rule_code,
max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].ruleName')))) as rule_name, max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].ruleName')))) as rule_name,
max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].riskLevel')))) as risk_level max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].riskLevel')))) as risk_level,
max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].reasonDetail')))) as reason_detail
from ccdi_project_overview_employee_result result from ccdi_project_overview_employee_result result
join ( join (
<include refid="jsonArrayIndexSql"/> <include refid="jsonArrayIndexSql"/>

View File

@@ -0,0 +1,22 @@
# 外部人员详情原因快照后端实施计划
## 背景
外部人员「查看详情」的对象异常明细由前端命中标签组装,但命中标签接口未返回 `reasonDetail`,导致对象卡片只能展示模型名和规则名,缺少和员工详情一致的异常原因快照。
## 实施内容
-`CcdiProjectRiskHitTagVO` 增加 `reasonDetail` 字段。
- 在外部人员风险来源 SQL 中透传 `ccdi_bank_statement_tag_result.reason_detail`
-`selectExternalRiskHitTagsByScope` 中按模型和规则聚合命中标签,并优先返回对象级原因快照;对象级为空时返回流水级原因快照。
- 同步在员工命中标签查询中映射 JSON 内的 `reasonDetail`,保持 VO 字段语义一致。
## 影响范围
- 外部人员预警列表详情、命中模型外部人员详情的 `riskPointTagList/hitTagList` 响应字段增加 `reasonDetail`
- 不改变原有筛选、排序、风险等级和模型统计逻辑。
## 验证
- 执行项目风险模型人员与对象明细相关 Mapper 测试。
- 执行后端模块编译或打包,确认 VO 与 XML 映射无编译和解析问题。

View File

@@ -0,0 +1,22 @@
# 资金流向边明细导出后端实施计划(已取消)
## 目标
需求已调整:资金流向逐笔流水明细不再增加“导出明细”,本后端计划不执行。
## 范围
- 不新增资金流向导出 Excel 对象。
- 不新增资金流向导出接口。
- 保持现有 `/ccdi/project/fund-graph/edge-detail` 分页查询不变。
## 实施步骤
1. 回收已准备的资金流向导出前后端改动。
2. 保持资金流向页签仅展示图谱和逐笔流水明细。
3. 将导出需求调整到“查看详情 > 异常明细”组件内实现。
## 验证
- 搜索确认不存在 `/edge-detail/export``exportFundGraphEdgeDetail``CcdiFundGraphStatementExcel`
- 后端不再因本需求产生新增接口。

View File

@@ -0,0 +1,21 @@
# 外部人员详情原因快照前端实施计划
## 背景
外部人员详情的「对象异常明细」展示风格已复用员工详情卡片,但内容缺少原因快照,生产页面显示为泛化文案。
## 实施内容
- 外部人员对象卡片优先展示命中标签中的 `reasonDetail`
- 流水标签补全时保留后端返回的 `reasonDetail`
- 历史数据缺少 `reasonDetail` 时,基于已加载的关联流水补充笔数、累计金额、最大金额、最近交易时间、主要对手方,避免只展示泛化命中文案。
## 影响范围
- 仅影响 `ExternalPersonDetailDialog.vue` 中「异常明细」页签的对象异常卡片内容。
- 不调整资金流向、员工详情布局、导出按钮位置和图谱样式。
## 验证
- 使用项目 Node 14.21.3 执行前端构建。
- 启动真实前后端后,在业务页面打开外部人员「查看详情」检查对象异常明细展示。

View File

@@ -0,0 +1,25 @@
# 项目分析异常明细导出前端实施计划
## 目标
在员工风险人员、命中模型涉及人员、外部人员三个“查看详情”入口的“异常明细”页签中增加简洁导出按钮,导出当前弹窗已加载的异常明细数据。
## 范围
- 修改共用组件 `ProjectAnalysisAbnormalTab.vue`
- 导出流水异常与对象异常两类明细。
- 不修改资金流向页签导出能力,资金流向不提供导出按钮。
## 实施步骤
1. 在异常明细页签顶部增加右对齐“导出”按钮。
2. 使用当前 `detailData.groups` 组装 CSV 数据,覆盖分页外的全部已加载记录。
3. 流水异常导出本方名称、本方账号、对手方名称、对手方账号、交易信息、异常标签和金额。
4. 对象异常导出分组、标题、摘要、异常说明、异常标签和补充字段。
5. 使用 UTF-8 BOM 生成 CSV保证 Excel 打开中文正常显示。
## 验证
- 前端构建通过。
- 搜索确认资金流向导出入口不存在。
- 员工与外部人员详情弹窗均复用该异常明细导出按钮。

View File

@@ -0,0 +1,28 @@
# 项目分析详情侧栏收起与异常明细导出前端实施计划
## 目标
在结果总览的项目分析详情弹窗中,减少左侧“人物档案、命中模型摘要”对屏幕宽度的占用,并为“查看详情 > 异常明细”增加导出入口。
## 范围
- 调整 `ProjectAnalysisDialog.vue` 的左右布局,支持左侧侧栏收起和展开。
- 调整 `ProjectAnalysisSidebar.vue` 的紧凑展示样式,使默认占宽更小。
- 调整 `ExternalPersonDetailDialog.vue`,保持外部人员详情与员工详情布局一致。
- 调整 `ProjectAnalysisAbnormalTab.vue`,在异常明细页签提供导出按钮。
- 资金流向页签不提供导出按钮。
## 实施步骤
1. 在项目分析弹窗中维护 `sidebarCollapsed` 状态,弹窗打开时默认展开。
2. 在弹窗布局中加入侧栏收起按钮,收起后左侧变为窄栏,右侧主区域占满剩余空间。
3. 将左侧默认宽度由百分比大侧栏改为固定紧凑宽度,降低对资金流向内容区的挤占。
4. 将弹窗头部压缩为单行低高度展示,去掉“结果总览”层级文案。
5. 在异常明细页签增加“导出”按钮,导出当前弹窗已加载的异常明细数据。
## 验证
- 前端构建或语法检查不报错。
- 真实页面打开项目分析详情后,侧栏可以收起和展开。
- 员工两个“查看详情”和外部人员“查看详情”均使用同一套紧凑布局。
- 异常明细页签可看到导出按钮,资金流向页签不出现导出按钮。

View File

@@ -0,0 +1,23 @@
# 外部人员详情对象异常原因快照实施记录
## 修改内容
- 后端 `CcdiProjectRiskHitTagVO` 增加 `reasonDetail` 字段。
- 后端外部人员风险命中标签查询返回 `reason_detail`,并优先取对象级原因快照。
- 前端外部人员详情对象异常卡片优先展示真实 `reasonDetail`
- 前端增加缺失原因快照时的流水统计兜底展示,包含关联流水、累计金额、最大金额、最近交易和主要对手方。
- 前端外部人员预警列表标准化命中标签时保留 `ruleCode``reasonDetail`,避免从列表进入详情时丢失原因快照。
## 影响范围
- 外部人员预警详情。
- 命中模型涉及外部人员的「查看详情」。
- 员工详情原有对象明细接口不变,仅命中标签 VO 支持原因字段。
## 验证情况
- 后端 Mapper 测试通过:`CcdiProjectOverviewMapperRiskModelPeopleTest``CcdiProjectOverviewMapperSqlTest` 共 10 个用例通过。
- 前端生产构建通过,存在项目既有包体积告警。
- 后端 `ruoyi-admin` 打包通过并已重启到 62318。
- 前端已用 Node 14.21.3 重启到 8080。
- 真实页面验证通过:外部人员详情「对象异常明细」已展示详细原因快照,例如“近一年流水交易额 52000.00 元,超过阈值 10000 元”,不再显示泛化命中文案。

View File

@@ -0,0 +1,20 @@
# 资金流向图谱图例与节点分类展示优化实施记录
## 修改内容
- 将资金流向图谱右上角图例由“人员、家庭关系、对手方、已展开”调整为“本人、关系人、其他方、已展开”。
- 将关系人节点从与本人相近的绿色人物三角改为橙色六边形人物节点,降低本人和关系人之间的视觉混淆。
- 在资金图谱节点 tooltip 中增加“类型”说明,显示本人、关系人、其他方或已展开。
## 影响范围
- 前端文件:`ruoyi-ui/src/views/ccdiProject/components/detail/graph/FundGraphSection.vue`
- 仅调整资金图谱展示文案、节点图标与 tooltip不修改接口、后端逻辑、SQL 或资金流向数据口径。
## 验证情况
- 已通过代码检查确认分类逻辑保持不变:中心节点仍为本人,有关系类型的节点仍归为关系人,已展开节点仍按展开状态展示,其余节点归为其他方。
- 已执行 `nvm use 14.21.3`,当前 Node 版本为 `v14.21.3`
- 已执行 `node node_modules\@vue\cli-service\bin\vue-cli-service.js build --mode production`,构建成功;仅存在既有资源体积 warning。
- 已在本地前端服务 `http://localhost:8080/` 进入真实项目详情页面,打开项目分析详情并切换至资金流向页签,资金图谱 canvas 正常渲染,页面无前端报错。
- 已检查资金图谱配置代码:图例为“本人、关系人、其他方、已展开”,关系人节点使用橙色六边形人物图标,节点 tooltip 输出“类型”说明。

View File

@@ -0,0 +1,24 @@
# 项目分析异常流水明细导出实施记录
## 修改内容
- 调整项目分析【查看详情】内异常明细导出逻辑,仅导出流水异常分组数据。
- 员工详情弹窗与外部人员详情弹窗均复用同一异常明细组件,本次调整同时覆盖两个入口。
- 移除导出结果中的对象异常记录,避免出现“对象异常 / 异常对象摘要”等风险对象相关内容。
- 精简导出字段,保留流水明细需要的列:分组、交易时间、本方名称、本方账号、对方名称、对方账号、摘要、交易类型、异常标签、交易金额。
- 导出文件名由“异常明细”调整为“异常流水明细”,空数据提示同步改为“暂无可导出的异常流水明细”。
## 影响范围
- 前端文件:`ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisAbnormalTab.vue`
- 覆盖入口:员工【查看详情】、外部人员【查看详情】
- 文档文件:`docs/reports/implementation/2026-07-01-project-analysis-abnormal-flow-export.md`
- 不涉及后端接口、数据库结构、菜单权限或风险明细主导出功能。
## 验证情况
- 已完成代码级检查,确认导出按钮只在流水异常分组展示。
- 已确认导出行构造逻辑只遍历 `BANK_STATEMENT` 分组,不再写入 `OBJECT` 对象异常记录。
- 已确认员工详情 `ProjectAnalysisDialog.vue` 与外部人员详情 `ExternalPersonDetailDialog.vue` 均引用该组件。
- 前端生产构建通过,存在项目既有包体积告警。
- 当前浏览器控制工具未暴露可调用入口,未完成页面点击下载验证。

View File

@@ -0,0 +1,34 @@
# 项目分析详情侧栏收起与异常明细导出实施记录
## 修改内容
1. 项目分析详情弹窗左侧“人物档案、命中模型摘要”默认改为紧凑宽度,并新增收起/展开按钮。
2. 侧栏收起后保留窄栏身份提示,右侧分析页签自动获得更多展示宽度。
3. 根据页面反馈进一步简化侧栏样式:左侧区域整体保持白底,档案区保留单层白色外框,不再呈现灰底缝隙、灰底承托或嵌套白卡,也不再把“人物档案、命中模型摘要”做成两个模块层级;收起按钮放入档案卡片内部并改为纯图标按钮。
4. 资金流向逐笔明细表补齐本方与对手方名称、账号展示,避免生产数据中名称/账号不易区分;资金流向不提供导出按钮。
5. 压缩项目分析详情与外部人员详情弹窗头部:移除“结果总览”层级文案,标题改为单行低高度展示。
6. 外部人员“查看详情”弹窗同步接入单卡侧栏、图标收起按钮和紧凑头部,确保员工风险人员、命中模型涉及人员、外部人员三个入口风格一致。
7. “查看详情 > 异常明细”页签在“流水异常明细”标题栏右侧新增“导出”按钮,导出当前弹窗已加载的流水异常和对象异常 CSV 明细。
8. 异常明细表将本方、对方列调整为“名称在上、账号在下”的展示方式,避免名称与账号含义混淆。
## 影响范围
- 前端页面:项目详情 > 结果总览 > 命中模型涉及人员 / 风险人员 / 外部人员查看详情。
- 前端组件:`ProjectAnalysisDialog.vue``ExternalPersonDetailDialog.vue``ProjectAnalysisSidebar.vue``ProjectAnalysisAbnormalTab.vue``FundGraphSection.vue`
- 后端模块:无新增接口;资金流图谱查询链路保持不变。
- 不修改资金边明细分页查询口径,不影响手工资金流向边。
## 验证情况
- 已执行:`mvn -pl ccdi-project -am compile -DskipTests`,通过。
- 已执行:按 `.nvmrc` 使用 Node `v14.21.3` 直接调用本地 `vue-cli-service build`,构建通过,仅存在既有资源体积警告。
- 已执行:侧栏单卡片与图标按钮微调后,再次执行同一前端构建,构建通过,仅存在既有资源体积警告。
- 已执行:异常明细导出、单层白色档案卡片、卡片内纯图标收起按钮调整后的前端构建,构建通过,仅存在既有资源体积警告。
- 已执行:真实页面 `http://localhost:8080/ccdiProject/detail/90342?tab=overview` 验证。
- 命中模型涉及人员“查看详情”可打开项目分析详情弹窗。
- 弹窗中出现档案卡片内纯图标收起按钮。
- 点击收起后,左侧侧栏由约 286px 收缩为约 54px右侧主区域由约 1211px 扩展为约 1453px。
- 已检查:资金流向页签可加载资金图接口 `/ccdi/project/fund-graph/graph`
- 已检查:资金流向页签不存在“导出明细”按钮和 `/edge-detail/export` 导出接口调用。
- 已执行:真实页面复验左侧区域、侧栏外层、右侧主区均为白色背景;档案区保留单层白色外框,外框外不再出现灰色缝隙;收起按钮在卡片内部且仅显示图标。
- 已执行:真实页面复验“导出”按钮位于“流水异常明细”标题栏右侧,不位于异常明细整体上层。

View File

@@ -12,7 +12,6 @@
<div class="project-analysis-header"> <div class="project-analysis-header">
<div class="project-analysis-header__main"> <div class="project-analysis-header__main">
<div class="project-analysis-header__title-group"> <div class="project-analysis-header__title-group">
<div class="project-analysis-header__eyebrow">结果总览</div>
<div class="project-analysis-header__title">外部人员详情</div> <div class="project-analysis-header__title">外部人员详情</div>
</div> </div>
<button class="project-analysis-header__close" type="button" aria-label="关闭" @click="closeDialog"> <button class="project-analysis-header__close" type="button" aria-label="关闭" @click="closeDialog">
@@ -21,12 +20,19 @@
</div> </div>
</div> </div>
<div class="project-analysis-workspace"> <div
<project-analysis-sidebar class="project-analysis-workspace"
class="project-analysis-layout__sidebar" :class="{ 'project-analysis-workspace--sidebar-collapsed': sidebarCollapsed }"
:sidebar-data="sidebarData" >
:field-labels="sidebarFieldLabels" <div class="project-analysis-layout__sidebar">
/> <project-analysis-sidebar
:sidebar-data="sidebarData"
:field-labels="sidebarFieldLabels"
collapsible
:collapsed="sidebarCollapsed"
@toggle="toggleSidebar"
/>
</div>
<div class="project-analysis-layout__main"> <div class="project-analysis-layout__main">
<el-alert <el-alert
@@ -100,6 +106,7 @@ export default {
data() { data() {
return { return {
activeTab: "abnormalDetail", activeTab: "abnormalDetail",
sidebarCollapsed: false,
detailLoading: false, detailLoading: false,
detailError: "", detailError: "",
statementRows: [], statementRows: [],
@@ -213,6 +220,7 @@ export default {
visible(value) { visible(value) {
if (value) { if (value) {
this.activeTab = "abnormalDetail"; this.activeTab = "abnormalDetail";
this.sidebarCollapsed = false;
this.loadStatementRows(); this.loadStatementRows();
} }
}, },
@@ -295,22 +303,91 @@ export default {
...tag, ...tag,
modelCode: tag.modelCode || (matched && matched.modelCode), modelCode: tag.modelCode || (matched && matched.modelCode),
modelName: tag.modelName || (matched && matched.modelName), modelName: tag.modelName || (matched && matched.modelName),
reasonDetail: tag.reasonDetail || (matched && matched.reasonDetail),
}; };
}, },
buildObjectAbnormalRecord(tag, index) { buildObjectAbnormalRecord(tag, index) {
const safeTag = typeof tag === "object" && tag ? tag : {}; const safeTag = typeof tag === "object" && tag ? tag : {};
const modelName = safeTag.modelName || "外部人员模型"; const modelName = safeTag.modelName || "外部人员模型";
const ruleName = safeTag.ruleName || this.formatRiskTag(tag) || "外部人员预警"; const ruleName = safeTag.ruleName || this.formatRiskTag(tag) || "外部人员预警";
const matchedRows = this.findMatchedStatementRows(safeTag);
const statementReason = this.buildStatementReasonDetail(ruleName, matchedRows);
return { return {
modelCode: safeTag.modelCode || `EXTERNAL_MODEL_${index}`, modelCode: safeTag.modelCode || `EXTERNAL_MODEL_${index}`,
title: ruleName, title: ruleName,
subtitle: modelName, subtitle: modelName,
riskTags: [modelName, this.formatRiskLevel(safeTag.riskLevel)].filter(Boolean), riskTags: [modelName, this.formatRiskLevel(safeTag.riskLevel)].filter(Boolean),
reasonDetail: safeTag.reasonDetail || this.buildReasonDetail(ruleName, modelName), reasonDetail: safeTag.reasonDetail || statementReason || this.buildReasonDetail(ruleName, modelName),
summary: ruleName, summary: ruleName,
extraFields: [], extraFields: this.buildObjectExtraFields(matchedRows),
}; };
}, },
findMatchedStatementRows(riskTag) {
const riskKeys = this.buildTagKeys(riskTag);
if (!riskKeys.length) {
return [];
}
return this.statementRows.filter((row) => {
const hitTags = Array.isArray(row && row.hitTags) ? row.hitTags : [];
return hitTags.some((tag) => this.buildTagKeys(tag).some((key) => riskKeys.includes(key)));
});
},
buildStatementReasonDetail(ruleName, rows) {
if (!Array.isArray(rows) || !rows.length) {
return "";
}
const amounts = rows.map((row) => Math.abs(Number(row && row.displayAmount) || 0));
const totalAmount = amounts.reduce((sum, amount) => sum + amount, 0);
const maxAmount = amounts.reduce((max, amount) => Math.max(max, amount), 0);
const latestTime = rows
.map((row) => row && row.trxDate)
.filter(Boolean)
.sort()
.pop();
const counterparties = this.collectCounterparties(rows);
const parts = [
`命中${ruleName}`,
`关联流水${rows.length}`,
`累计交易金额${this.formatAmount(totalAmount)}`,
`单笔最大金额${this.formatAmount(maxAmount)}`,
latestTime ? `最近交易时间${latestTime}` : "",
counterparties ? `主要对手方:${counterparties}` : "",
].filter(Boolean);
return parts.join("");
},
buildObjectExtraFields(rows) {
if (!Array.isArray(rows) || !rows.length) {
return [];
}
const amounts = rows.map((row) => Math.abs(Number(row && row.displayAmount) || 0));
const totalAmount = amounts.reduce((sum, amount) => sum + amount, 0);
const maxAmount = amounts.reduce((max, amount) => Math.max(max, amount), 0);
const latestTime = rows
.map((row) => row && row.trxDate)
.filter(Boolean)
.sort()
.pop();
return [
{ label: "关联流水", value: `${rows.length}` },
{ label: "累计金额", value: `${this.formatAmount(totalAmount)}` },
{ label: "最大金额", value: `${this.formatAmount(maxAmount)}` },
latestTime ? { label: "最近交易", value: latestTime } : null,
{ label: "主要对手方", value: this.collectCounterparties(rows) || "-" },
].filter(Boolean);
},
collectCounterparties(rows) {
const names = [];
rows.forEach((row) => {
const value = row && (row.customerAccountName || row.customerAccountNo);
if (value && !names.includes(value)) {
names.push(value);
}
});
return names.slice(0, 3).join("、");
},
formatAmount(value) {
return (Number(value) || 0).toFixed(2);
},
buildReasonDetail(ruleName, modelName) { buildReasonDetail(ruleName, modelName) {
const parts = [ const parts = [
this.displayName, this.displayName,
@@ -343,8 +420,18 @@ export default {
closeDialog() { closeDialog() {
this.visibleProxy = false; this.visibleProxy = false;
}, },
toggleSidebar() {
this.sidebarCollapsed = !this.sidebarCollapsed;
this.$nextTick(() => {
const fundFlowTab = this.$refs.fundFlowTab;
if (fundFlowTab && fundFlowTab.ensureGraphReady) {
fundFlowTab.ensureGraphReady();
}
});
},
handleClosed() { handleClosed() {
this.activeTab = "abnormalDetail"; this.activeTab = "abnormalDetail";
this.sidebarCollapsed = false;
this.detailLoading = false; this.detailLoading = false;
this.detailError = ""; this.detailError = "";
this.statementRows = []; this.statementRows = [];
@@ -372,35 +459,27 @@ export default {
} }
.project-analysis-header { .project-analysis-header {
padding: 28px 40px 24px; padding: 14px 24px;
border-bottom: 1px solid #dde3ec; border-bottom: 1px solid #dde3ec;
background: #ffffff; background: #ffffff;
} }
.project-analysis-header__main { .project-analysis-header__main {
display: flex; display: flex;
align-items: flex-end; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 24px; gap: 16px;
} }
.project-analysis-header__title-group { .project-analysis-header__title-group {
min-width: 0; min-width: 0;
} }
.project-analysis-header__eyebrow {
color: #65758d;
font-size: 15px;
font-weight: 700;
line-height: 1;
}
.project-analysis-header__title { .project-analysis-header__title {
margin-top: 18px;
color: #101a2b; color: #101a2b;
font-size: 28px; font-size: 20px;
font-weight: 700; font-weight: 700;
line-height: 1; line-height: 28px;
} }
.project-analysis-header__close { .project-analysis-header__close {
@@ -425,24 +504,41 @@ export default {
.project-analysis-workspace { .project-analysis-workspace {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 36px; gap: 0;
min-height: 640px; min-height: 640px;
max-height: calc(92vh - 150px); max-height: calc(92vh - 96px);
padding: 20px 40px 28px; padding: 20px 28px 28px;
overflow: auto; overflow: auto;
background: #ffffff; background: #ffffff;
} }
.project-analysis-workspace--sidebar-collapsed {
gap: 0;
}
.project-analysis-layout__sidebar { .project-analysis-layout__sidebar {
flex: 0 0 34%; position: sticky;
max-width: 460px; top: 0;
flex: 0 0 286px;
max-width: 286px;
background: #ffffff;
}
.project-analysis-workspace--sidebar-collapsed .project-analysis-layout__sidebar {
flex-basis: 46px;
max-width: 46px;
} }
.project-analysis-layout__main { .project-analysis-layout__main {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
border-left: 1px solid #dde3ec; border-left: 0;
padding-left: 36px; padding-left: 24px;
background: #ffffff;
}
.project-analysis-workspace--sidebar-collapsed .project-analysis-layout__main {
padding-left: 18px;
} }
.project-analysis-tabs { .project-analysis-tabs {

View File

@@ -8,6 +8,16 @@
> >
<div class="abnormal-card__header"> <div class="abnormal-card__header">
<div class="abnormal-card__title">{{ group.groupName || "异常明细" }}</div> <div class="abnormal-card__title">{{ group.groupName || "异常明细" }}</div>
<el-button
v-if="shouldShowExportButton(group)"
size="mini"
type="primary"
plain
icon="el-icon-download"
@click="handleExportAbnormalDetail"
>
导出
</el-button>
</div> </div>
<div class="abnormal-card__content"> <div class="abnormal-card__content">
@@ -17,15 +27,15 @@
class="abnormal-table" class="abnormal-table"
> >
<el-table-column prop="trxDate" label="交易时间" min-width="160" /> <el-table-column prop="trxDate" label="交易时间" min-width="160" />
<el-table-column label="本方账户" min-width="220"> <el-table-column label="本方信息" min-width="220">
<template slot-scope="scope"> <template slot-scope="scope">
<div class="multi-line-cell"> <div class="multi-line-cell">
<div class="primary-text">{{ scope.row.leAccountNo || "-" }}</div> <div class="primary-text">{{ scope.row.leAccountName || "-" }}</div>
<div class="secondary-text">{{ scope.row.leAccountName || "-" }}</div> <div class="secondary-text">{{ scope.row.leAccountNo || "-" }}</div>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="对方账户" min-width="220"> <el-table-column label="对方信息" min-width="220">
<template slot-scope="scope"> <template slot-scope="scope">
<div class="multi-line-cell"> <div class="multi-line-cell">
<div class="primary-text">{{ scope.row.customerAccountName || "-" }}</div> <div class="primary-text">{{ scope.row.customerAccountName || "-" }}</div>
@@ -126,6 +136,7 @@
</template> </template>
<script> <script>
import { saveAs } from "file-saver";
import { buildModelEvidenceFingerprint, MODEL_EVIDENCE_FINGERPRINT_RULE } from "@/utils/ccdiEvidence"; import { buildModelEvidenceFingerprint, MODEL_EVIDENCE_FINGERPRINT_RULE } from "@/utils/ccdiEvidence";
export default { export default {
@@ -194,6 +205,82 @@ export default {
const groupKey = this.resolveGroupKey(group); const groupKey = this.resolveGroupKey(group);
this.$set(this.statementPageMap, groupKey, page); this.$set(this.statementPageMap, groupKey, page);
}, },
shouldShowExportButton(group) {
return group && group.groupType === "BANK_STATEMENT";
},
handleExportAbnormalDetail() {
const rows = this.buildAbnormalExportRows();
if (!rows.length) {
this.$message.warning("暂无可导出的异常流水明细");
return;
}
const headers = [
"分组",
"交易时间",
"本方名称",
"本方账号",
"对手方名称",
"对手方账号",
"摘要",
"交易类型",
"异常标签",
"交易金额",
];
const csvContent = [headers, ...rows]
.map((row) => row.map((cell) => this.escapeCsvCell(cell)).join(","))
.join("\r\n");
const blob = new Blob([`\uFEFF${csvContent}`], { type: "text/csv;charset=utf-8;" });
const fileName = `${this.sanitizeFileName(`异常流水明细_${this.resolvePersonName()}`)}.csv`;
saveAs(blob, fileName);
},
buildAbnormalExportRows() {
const rows = [];
this.detailGroups.forEach((group) => {
if (!group || group.groupType !== "BANK_STATEMENT") {
return;
}
const records = Array.isArray(group && group.records) ? group.records : [];
records.forEach((record) => {
rows.push([
group.groupName || "",
record.trxDate,
record.leAccountName,
record.leAccountNo,
record.customerAccountName,
record.customerAccountNo,
record.userMemo,
record.cashType,
this.formatRiskTags(record.hitTags),
record.displayAmount,
]);
});
});
return rows;
},
formatRiskTags(tags) {
if (!Array.isArray(tags)) {
return "";
}
return tags
.map((tag) => {
if (tag && typeof tag === "object") {
return tag.ruleName || tag.name || tag.label || tag.title || "";
}
return tag;
})
.filter((tag) => tag !== null && tag !== undefined && `${tag}`.trim() !== "")
.join("、");
},
escapeCsvCell(value) {
const text = value === null || value === undefined ? "" : `${value}`;
return `"${text.replace(/"/g, '""')}"`;
},
sanitizeFileName(value) {
return `${value || "异常明细"}`.replace(/[\\/:*?"<>|]/g, "_");
},
resolvePersonName() {
return (this.person && (this.person.name || this.person.staffName || this.person.personName)) || "项目分析";
},
handleAddModelEvidence(item, group) { handleAddModelEvidence(item, group) {
const safeItem = item || {}; const safeItem = item || {};
const safeGroup = group || {}; const safeGroup = group || {};
@@ -254,6 +341,10 @@ export default {
} }
.abnormal-card__header { .abnormal-card__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 0; margin-bottom: 0;
padding: 22px 30px; padding: 22px 30px;
border-bottom: 1px solid #e3eaf3; border-bottom: 1px solid #e3eaf3;

View File

@@ -12,7 +12,6 @@
<div class="project-analysis-header"> <div class="project-analysis-header">
<div class="project-analysis-header__main"> <div class="project-analysis-header__main">
<div class="project-analysis-header__title-group"> <div class="project-analysis-header__title-group">
<div class="project-analysis-header__eyebrow">结果总览</div>
<div class="project-analysis-header__title">项目分析详情</div> <div class="project-analysis-header__title">项目分析详情</div>
</div> </div>
<div <div
@@ -29,11 +28,19 @@
</button> </button>
</div> </div>
</div> </div>
<div v-loading="detailLoading" class="project-analysis-workspace"> <div
<project-analysis-sidebar v-loading="detailLoading"
class="project-analysis-layout__sidebar" class="project-analysis-workspace"
:sidebar-data="dialogData.sidebar" :class="{ 'project-analysis-workspace--sidebar-collapsed': sidebarCollapsed }"
/> >
<div class="project-analysis-layout__sidebar">
<project-analysis-sidebar
:sidebar-data="dialogData.sidebar"
collapsible
:collapsed="sidebarCollapsed"
@toggle="toggleSidebar"
/>
</div>
<div class="project-analysis-layout__main"> <div class="project-analysis-layout__main">
<el-alert <el-alert
v-if="detailError" v-if="detailError"
@@ -137,6 +144,7 @@ export default {
data() { data() {
return { return {
activeTab: "abnormalDetail", activeTab: "abnormalDetail",
sidebarCollapsed: false,
detailLoading: false, detailLoading: false,
detailError: "", detailError: "",
detailData: null, detailData: null,
@@ -182,6 +190,7 @@ export default {
methods: { methods: {
resetDialogState() { resetDialogState() {
this.activeTab = "abnormalDetail"; this.activeTab = "abnormalDetail";
this.sidebarCollapsed = false;
this.detailLoading = false; this.detailLoading = false;
this.detailError = ""; this.detailError = "";
this.detailData = null; this.detailData = null;
@@ -230,6 +239,17 @@ export default {
closeDialog() { closeDialog() {
this.visibleProxy = false; this.visibleProxy = false;
}, },
toggleSidebar() {
this.sidebarCollapsed = !this.sidebarCollapsed;
this.$nextTick(() => {
const tabRef = this.activeTab === "relationshipGraph"
? this.$refs.relationshipGraphTab
: this.$refs.fundFlowTab;
if (tabRef && tabRef.ensureGraphReady) {
tabRef.ensureGraphReady();
}
});
},
handleTabChange() { handleTabChange() {
this.$nextTick(() => { this.$nextTick(() => {
const tabRef = this.activeTab === "relationshipGraph" const tabRef = this.activeTab === "relationshipGraph"
@@ -255,42 +275,34 @@ export default {
} }
.project-analysis-header { .project-analysis-header {
padding: 28px 40px 24px; padding: 14px 24px;
border-bottom: 1px solid #dde3ec; border-bottom: 1px solid #dde3ec;
background: #ffffff; background: #ffffff;
} }
.project-analysis-header__main { .project-analysis-header__main {
display: flex; display: flex;
align-items: flex-end; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 24px; gap: 16px;
} }
.project-analysis-header__title-group { .project-analysis-header__title-group {
min-width: 0; min-width: 0;
} }
.project-analysis-header__eyebrow {
color: #65758d;
font-size: 15px;
font-weight: 700;
line-height: 1;
}
.project-analysis-header__title { .project-analysis-header__title {
margin-top: 18px;
color: #101a2b; color: #101a2b;
font-size: 28px; font-size: 20px;
font-weight: 700; font-weight: 700;
line-height: 1; line-height: 28px;
} }
.project-analysis-header__meta { .project-analysis-header__meta {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
min-height: 34px; min-height: 30px;
padding: 0 12px; padding: 0 12px;
border: 1px solid #bfd0e2; border: 1px solid #bfd0e2;
border-radius: 2px; border-radius: 2px;
@@ -331,14 +343,18 @@ export default {
.project-analysis-workspace { .project-analysis-workspace {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 36px; gap: 0;
min-height: 640px; min-height: 640px;
max-height: calc(92vh - 150px); max-height: calc(92vh - 96px);
padding: 20px 40px 28px; padding: 20px 28px 28px;
overflow: auto; overflow: auto;
background: #ffffff; background: #ffffff;
} }
.project-analysis-workspace--sidebar-collapsed {
gap: 0;
}
.project-analysis-layout { .project-analysis-layout {
display: flex; display: flex;
gap: 20px; gap: 20px;
@@ -346,15 +362,28 @@ export default {
} }
.project-analysis-layout__sidebar { .project-analysis-layout__sidebar {
flex: 0 0 34%; position: sticky;
max-width: 460px; top: 0;
flex: 0 0 286px;
max-width: 286px;
background: #ffffff;
}
.project-analysis-workspace--sidebar-collapsed .project-analysis-layout__sidebar {
flex-basis: 46px;
max-width: 46px;
} }
.project-analysis-layout__main { .project-analysis-layout__main {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
border-left: 1px solid #dde3ec; border-left: 0;
padding-left: 36px; padding-left: 24px;
background: #ffffff;
}
.project-analysis-workspace--sidebar-collapsed .project-analysis-layout__main {
padding-left: 18px;
} }
.project-analysis-layout__alert { .project-analysis-layout__alert {
@@ -370,7 +399,7 @@ export default {
.project-analysis-dialog { .project-analysis-dialog {
margin-top: 2vh !important; margin-top: 2vh !important;
border-radius: 0; border-radius: 0;
background: #f5f6f8; background: #ffffff;
overflow: hidden; overflow: hidden;
.el-dialog__header { .el-dialog__header {
@@ -379,7 +408,7 @@ export default {
.el-dialog__body { .el-dialog__body {
padding: 0; padding: 0;
background: #f5f6f8; background: #ffffff;
} }
} }

View File

@@ -1,6 +1,30 @@
<template> <template>
<aside class="project-analysis-sidebar"> <aside class="project-analysis-sidebar" :class="{ 'is-collapsed': collapsed }">
<div class="sidebar-profile-card"> <div v-if="collapsed" class="sidebar-collapsed-card">
<button
v-if="collapsible"
class="sidebar-card-toggle"
type="button"
title="展开"
aria-label="展开"
@click="$emit('toggle')"
>
<i class="el-icon-arrow-right" />
</button>
<span>人物档案</span>
<strong>{{ sidebarData.basicInfo.name || "-" }}</strong>
</div>
<div v-else class="sidebar-profile-card">
<button
v-if="collapsible"
class="sidebar-card-toggle"
type="button"
title="收起"
aria-label="收起"
@click="$emit('toggle')"
>
<i class="el-icon-arrow-left" />
</button>
<section class="sidebar-profile"> <section class="sidebar-profile">
<div class="sidebar-profile__identity"> <div class="sidebar-profile__identity">
<div class="sidebar-profile__identity-label">人物档案</div> <div class="sidebar-profile__identity-label">人物档案</div>
@@ -26,13 +50,12 @@
</section> </section>
<section class="sidebar-summary"> <section class="sidebar-summary">
<div class="sidebar-summary__title">命中模型摘要</div>
<div class="sidebar-summary__count"> <div class="sidebar-summary__count">
<span class="sidebar-summary__count-label">命中模型</span> <span class="sidebar-summary__count-label">命中模型</span>
<span class="sidebar-summary__count-value">{{ sidebarData.modelSummary.modelCount || "-" }}</span> <span class="sidebar-summary__count-value">{{ sidebarData.modelSummary.modelCount || "-" }}</span>
</div> </div>
<div class="sidebar-summary__tags"> <div class="sidebar-summary__tags">
<span class="sidebar-profile__label">核心异常标签</span> <span class="sidebar-profile__label">异常标签</span>
<div v-if="sidebarData.modelSummary.riskTags.length" class="sidebar-tag-list"> <div v-if="sidebarData.modelSummary.riskTags.length" class="sidebar-tag-list">
<el-tag <el-tag
v-for="(tag, index) in sidebarData.modelSummary.riskTags" v-for="(tag, index) in sidebarData.modelSummary.riskTags"
@@ -71,6 +94,14 @@ export default {
projectName: "所属项目", projectName: "所属项目",
}), }),
}, },
collapsible: {
type: Boolean,
default: false,
},
collapsed: {
type: Boolean,
default: false,
},
}, },
computed: { computed: {
normalizedFieldLabels() { normalizedFieldLabels() {
@@ -97,32 +128,67 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.project-analysis-sidebar { .project-analysis-sidebar {
flex: 1;
min-width: 0;
width: 100%; width: 100%;
align-self: flex-start; align-self: flex-start;
}
.sidebar-profile-card {
border: 1px solid #dde3ec;
border-radius: 3px;
background: #ffffff; background: #ffffff;
} }
.project-analysis-sidebar.is-collapsed {
width: 46px;
}
.sidebar-profile-card {
position: relative;
border: 1px solid #dfe6ef;
border-radius: 3px;
background: #ffffff;
box-shadow: none;
}
.sidebar-card-toggle {
position: absolute;
top: 10px;
right: 10px;
z-index: 2;
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 0;
border: 1px solid #d5dfeb;
border-radius: 2px;
background: #ffffff;
color: #4f6077;
font-size: 12px;
cursor: pointer;
}
.sidebar-card-toggle:hover {
border-color: #9db7d4;
color: #245b8f;
background: #eef5fb;
}
.sidebar-profile { .sidebar-profile {
padding: 34px 42px 28px; padding: 22px 22px 6px;
border-bottom: 1px solid #edf1f5; border-bottom: 0;
} }
.sidebar-summary { .sidebar-summary {
padding: 30px 42px 34px; padding: 0 22px 22px;
border-top: 1px solid #dde3ec; border-top: 0;
background: #fcfdfe; background: transparent;
} }
.sidebar-profile__identity-label { .sidebar-profile__identity-label {
padding-right: 32px;
color: #637187; color: #637187;
font-size: 16px; font-size: 14px;
font-weight: 700; font-weight: 700;
margin-bottom: 28px; margin-bottom: 18px;
} }
.sidebar-profile__name-row { .sidebar-profile__name-row {
@@ -134,39 +200,41 @@ export default {
.sidebar-profile__name { .sidebar-profile__name {
color: #111827; color: #111827;
font-size: 34px; font-size: 26px;
font-weight: 700; font-weight: 700;
line-height: 1.2; line-height: 1.2;
min-width: 0;
word-break: break-all;
} }
.sidebar-risk-badge { .sidebar-risk-badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-width: 102px; min-width: 74px;
height: 44px; height: 32px;
padding: 0 16px; padding: 0 10px;
border: 1px solid #f0c6c1; border: 1px solid #f0c6c1;
border-radius: 2px; border-radius: 2px;
background: #fff3f2; background: #fff3f2;
color: #c43d33; color: #c43d33;
font-size: 17px; font-size: 14px;
font-weight: 600; font-weight: 600;
line-height: 42px; line-height: 30px;
text-align: center; text-align: center;
} }
.sidebar-profile__meta { .sidebar-profile__meta {
display: grid; display: grid;
gap: 0; gap: 0;
margin-top: 30px; margin-top: 20px;
} }
.sidebar-profile__item { .sidebar-profile__item {
display: grid; display: grid;
grid-template-columns: 116px minmax(0, 1fr); grid-template-columns: 72px minmax(0, 1fr);
gap: 16px; gap: 10px;
padding: 18px 0; padding: 12px 0;
border-bottom: 1px solid #edf1f5; border-bottom: 1px solid #edf1f5;
} }
@@ -177,46 +245,38 @@ export default {
.sidebar-profile__label, .sidebar-profile__label,
.sidebar-summary__count-label { .sidebar-summary__count-label {
color: #637187; color: #637187;
font-size: 16px; font-size: 13px;
} }
.sidebar-profile__value, .sidebar-profile__value,
.sidebar-summary__count-value { .sidebar-summary__count-value {
color: #172033; color: #172033;
font-size: 17px; font-size: 14px;
font-weight: 500; font-weight: 500;
line-height: 1.4; line-height: 1.4;
word-break: break-all; word-break: break-all;
} }
.sidebar-summary__title {
margin: 0 0 24px;
color: #223047;
font-size: 18px;
font-weight: 700;
}
.sidebar-summary__count { .sidebar-summary__count {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
gap: 16px; gap: 10px;
margin-top: 0; margin: 4px 0 14px;
margin-bottom: 28px;
} }
.sidebar-summary__tags { .sidebar-summary__tags {
display: grid; display: grid;
gap: 16px; gap: 10px;
margin-top: 0; margin-top: 0;
} }
.sidebar-summary__count-label { .sidebar-summary__count-label {
font-size: 17px; font-size: 13px;
} }
.sidebar-summary__count-value { .sidebar-summary__count-value {
color: #172033; color: #172033;
font-size: 40px; font-size: 24px;
font-weight: 700; font-weight: 700;
line-height: 1; line-height: 1;
} }
@@ -229,14 +289,52 @@ export default {
} }
.sidebar-tag-list ::v-deep(.el-tag) { .sidebar-tag-list ::v-deep(.el-tag) {
height: 38px; height: 28px;
padding: 0 16px; padding: 0 10px;
border: 1px solid #cad8eb; border: 1px solid #cad8eb;
border-radius: 2px; border-radius: 2px;
background: #ffffff; background: #ffffff;
color: #245b8f; color: #245b8f;
font-size: 15px; font-size: 12px;
font-weight: 600; font-weight: 600;
line-height: 36px; line-height: 26px;
}
.sidebar-collapsed-card {
position: relative;
display: flex;
align-items: center;
gap: 8px;
min-height: 220px;
width: 46px;
padding: 44px 0 14px;
border: 1px solid #dfe6ef;
border-radius: 3px;
background: #ffffff;
color: #637187;
writing-mode: vertical-rl;
text-orientation: mixed;
}
.sidebar-collapsed-card .sidebar-card-toggle {
top: 10px;
left: 50%;
right: auto;
transform: translateX(-50%);
writing-mode: horizontal-tb;
}
.sidebar-collapsed-card span {
font-size: 13px;
font-weight: 700;
}
.sidebar-collapsed-card strong {
max-height: 136px;
color: #172033;
font-size: 16px;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
} }
</style> </style>

View File

@@ -200,6 +200,8 @@ function normalizeRiskPointTags(tags, riskPoint, riskLevel) {
riskLevel: item.riskLevel || riskLevel, riskLevel: item.riskLevel || riskLevel,
modelCode: item.modelCode || "", modelCode: item.modelCode || "",
modelName: item.modelName || "", modelName: item.modelName || "",
ruleCode: item.ruleCode || "",
reasonDetail: item.reasonDetail || "",
}; };
} }
return null; return null;

View File

@@ -148,15 +148,15 @@
</div> </div>
</div> </div>
<div v-else class="detail-panel edge-panel"> <div v-else class="detail-panel edge-panel">
<div class="detail-head"> <div class="detail-head">
<div> <div>
<div class="detail-title">{{ selectedEdgeTitle }}</div> <div class="detail-title">{{ selectedEdgeTitle }}</div>
<div class="detail-subtitle"> <div class="detail-subtitle">
{{ isManualEdge(selectedEdge) ? "手工录入汇总边,不提供逐笔流水下钻" : "主体汇总边,下钻展示账户层逐笔流水" }} {{ isManualEdge(selectedEdge) ? "手工录入汇总边,不提供逐笔流水下钻" : "主体汇总边,下钻展示账户层逐笔流水" }}
</div>
</div> </div>
</div> </div>
</div>
<div v-if="showEdgeMetrics" class="edge-metrics"> <div v-if="showEdgeMetrics" class="edge-metrics">
<div> <div>
@@ -209,10 +209,16 @@
<el-empty :image-size="72" description="暂无流水明细" /> <el-empty :image-size="72" description="暂无流水明细" />
</template> </template>
<el-table-column label="交易日期" prop="trxDate" width="132" /> <el-table-column label="交易日期" prop="trxDate" width="132" />
<el-table-column label="本方名称" prop="leAccountName" min-width="92" show-overflow-tooltip /> <el-table-column label="本方信息" min-width="132" show-overflow-tooltip>
<el-table-column label="对手方名称" min-width="98" show-overflow-tooltip>
<template slot-scope="scope"> <template slot-scope="scope">
{{ scope.row.customerAccountName || "-" }} <div class="account-main">{{ scope.row.leAccountName || "-" }}</div>
<div class="account-sub">{{ scope.row.leAccountNo || "-" }}</div>
</template>
</el-table-column>
<el-table-column label="对手方信息" min-width="142" show-overflow-tooltip>
<template slot-scope="scope">
<div class="account-main">{{ scope.row.customerAccountName || "-" }}</div>
<div class="account-sub">{{ scope.row.customerAccountNo || "-" }}</div>
<el-tag <el-tag
v-if="scope.row.familyRelationType" v-if="scope.row.familyRelationType"
size="mini" size="mini"
@@ -483,6 +489,13 @@ const COMPANY_SYMBOL = svgSymbol(`
<path d="M20 24 H44 M20 34 H44 M20 44 H35" stroke="#b88745" stroke-width="4" stroke-linecap="round" /> <path d="M20 24 H44 M20 34 H44 M20 44 H35" stroke="#b88745" stroke-width="4" stroke-linecap="round" />
</svg>`); </svg>`);
const RELATION_SYMBOL = svgSymbol(`
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<path d="M32 6 L55 19 V45 L32 58 L9 45 V19 Z" fill="#fff6ec" stroke="#d98b3c" stroke-width="3.5" />
<circle cx="32" cy="27" r="8" fill="#d98b3c" />
<path d="M18 47 C20 37 25 33 32 33 C39 33 44 37 46 47 Z" fill="#d98b3c" />
</svg>`);
export default { export default {
name: "FundGraphSection", name: "FundGraphSection",
props: { props: {
@@ -1380,7 +1393,7 @@ export default {
return `${edge.fromName}${edge.toName}<br/>金额:${this.formatMoney(edge.totalAmount)}<br/>笔数:${edge.transactionCount || 0}`; return `${edge.fromName}${edge.toName}<br/>金额:${this.formatMoney(edge.totalAmount)}<br/>笔数:${edge.transactionCount || 0}`;
} }
const node = params.data.raw; const node = params.data.raw;
return `${node.nodeName}<br/>object_key${node.objectKey || "-"}`; return `${node.nodeName}<br/>类型:${this.getFundNodeCategoryName(params.data.category)}<br/>object_key${node.objectKey || "-"}`;
}, },
}, },
legend: { legend: {
@@ -1389,7 +1402,7 @@ export default {
itemWidth: 10, itemWidth: 10,
itemHeight: 10, itemHeight: 10,
textStyle: { color: "#606f82" }, textStyle: { color: "#606f82" },
data: ["人", "家庭关系", "对手方", "已展开"], data: ["人", "关系", "其他方", "已展开"],
}, },
series: [{ series: [{
type: "graph", type: "graph",
@@ -1400,9 +1413,9 @@ export default {
right: 56, right: 56,
bottom: 34, bottom: 34,
categories: [ categories: [
{ name: "人", itemStyle: { color: "#48a57f" } }, { name: "人", itemStyle: { color: "#3f9f78" } },
{ name: "家庭关系", itemStyle: { color: "#7fc56d" } }, { name: "关系", itemStyle: { color: "#d98b3c" } },
{ name: "对手方", itemStyle: { color: "#4f7fbd" } }, { name: "其他方", itemStyle: { color: "#4f7fbd" } },
{ name: "已展开", itemStyle: { color: "#95a3b6" } }, { name: "已展开", itemStyle: { color: "#95a3b6" } },
], ],
edgeSymbol: ["none", "arrow"], edgeSymbol: ["none", "arrow"],
@@ -1679,14 +1692,29 @@ export default {
}; };
}, },
getNodeSymbol(category) { getNodeSymbol(category) {
if (category === 0 || category === 1) { if (category === 0) {
return PERSON_SYMBOL; return PERSON_SYMBOL;
} }
if (category === 1) {
return RELATION_SYMBOL;
}
if (category === 3) { if (category === 3) {
return EXPANDED_SYMBOL; return EXPANDED_SYMBOL;
} }
return PROXY_SYMBOL; return PROXY_SYMBOL;
}, },
getFundNodeCategoryName(category) {
if (category === 0) {
return "本人";
}
if (category === 1) {
return "关系人";
}
if (category === 3) {
return "已展开";
}
return "其他方";
},
getNodeStyle(category, selected, dimmed = false) { getNodeStyle(category, selected, dimmed = false) {
const isCenter = category === 0; const isCenter = category === 0;
return { return {
@@ -2256,12 +2284,17 @@ export default {
.detail-head { .detail-head {
display: flex; display: flex;
align-items: flex-start;
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
padding-bottom: 12px; padding-bottom: 12px;
border-bottom: 1px solid #e5edf5; border-bottom: 1px solid #e5edf5;
} }
.detail-head > div {
min-width: 0;
}
.detail-title { .detail-title {
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
@@ -2274,6 +2307,21 @@ export default {
color: #8a99ad; color: #8a99ad;
} }
.account-main,
.memo-main {
color: #24364f;
font-weight: 600;
line-height: 1.35;
}
.account-sub,
.memo-sub {
margin-top: 2px;
color: #7a8aa0;
font-size: 12px;
line-height: 1.3;
}
.node-field-list { .node-field-list {
margin: 14px 0; margin: 14px 0;
border: 1px solid #dfeaf5; border: 1px solid #dfeaf5;