优化项目详情资金流向图谱展示

This commit is contained in:
wjj
2026-06-04 18:01:55 +08:00
parent a5eba12ed5
commit 4e90e22ee2
5 changed files with 349 additions and 49 deletions

View File

@@ -0,0 +1,21 @@
# 结果总览弹窗资金流向逐笔流水展示前端实施计划
## 需求范围
- 修改结果总览“查看详情”弹窗中的“资金流向”页签。
- 去掉资金边详情中的“弹窗速览”提示文案。
- 在弹窗资金边详情中展示逐笔流水明细,并保持分页加载。
- 不修改专项排查页的图谱入口和完整图谱展示逻辑。
## 实施方案
1. 调整 `ProjectAnalysisFundFlowTab.vue` 中传给 `FundGraphSection` 的参数,开启资金边逐笔流水表格。
2. 保持 `FundGraphSection` 现有边明细接口调用逻辑不变,继续使用分页查询。
3. 在弹窗包装组件内改为上方图谱、下方逐笔流水布局,收敛表格和分页样式,避免逐笔流水表撑高或挤压图谱画布。
4. 不新增接口、不修改后端、不改变专项排查页完整下钻能力。
## 验证计划
- 前端构建前按项目规则执行 `nvm use` 并确认 Node 版本。
- 执行前端构建或聚焦测试,确认组件编译通过。
- 使用真实页面打开结果总览“查看详情”弹窗,切换到“资金流向”,点击资金边确认下方显示逐笔流水和分页。

View File

@@ -0,0 +1,36 @@
# 结果总览弹窗资金流向可用性优化前端实施计划
## 目标
- 优化结果总览“查看详情”弹窗内“资金流向”页签的图谱展示空间。
- 降低多条资金边金额标签、节点名称在小画布中重叠的问题。
- 在查看单个对手方资金边明细后,通过点击图谱画布空白区域恢复全量图谱状态。
## 实施范围
- 仅调整结果总览“查看详情”弹窗中的资金流向图谱。
- 不调整专项排查页资金图谱的默认尺寸和业务逻辑。
- 不修改后端接口、数据库和资金流水分页接口。
## 实施方案
1.`ProjectAnalysisFundFlowTab` 中扩大资金流向工作区尺寸:
- 提高弹窗内图谱卡片高度。
- 改为上方图谱、下方逐笔流水布局,给图谱保留更大画布空间。
- 下方逐笔流水表格保持固定高度和分页展示,避免撑高弹窗。
2.`FundGraphSection` 中增加弹窗可配置能力:
- 增加边标签紧凑展示开关,金额使用“万/亿”等短格式展示。
- 支持隐藏资金边汇总卡片,只保留逐笔流水明细。
- 点击图谱画布空白区域时清空选中节点、选中边和逐笔流水明细,并重新渲染图谱。
3. 优化选中状态表达:
- 图谱中当前边和两端节点保持高亮,其他边降低透明度。
- 点空白区域后恢复初始全量图谱状态,不额外增加按钮。
## 验证计划
- 执行前端构建,确认无编译错误。
- 在真实页面进入结果总览“查看详情”弹窗,切换到“资金流向”。
- 选择包含多笔交易金额标签的资金边,验证节点名称和金额标签不再严重叠加,逐笔流水显示在图谱下方。
- 点击图谱画布空白区域,验证逐笔流水清空,图谱恢复全量状态。

View File

@@ -0,0 +1,30 @@
# 结果总览弹窗资金流向逐笔流水展示实施记录
## 修改内容
- 去除结果总览“查看详情”弹窗资金边详情中的“弹窗速览”提示。
- 结果总览弹窗“资金流向”页签启用逐笔流水表格展示。
- 保持逐笔流水按现有接口分页加载,不一次性拉取全量数据。
- 针对弹窗场景改为上方大图谱、下方逐笔流水明细布局,避免详情栏挤压图谱画布。
- 资金边金额标签改为“万/亿”短格式,选中边时高亮当前边和两端节点,其余关系降透明。
- 单边明细不再展示“返回全量”按钮,改为点击图谱画布空白区域恢复到刚进入“资金流向”页签时的全量图谱第一眼状态。
- 空白点击恢复全量时同步清除选中边、选中节点、逐笔流水数据、明细加载状态和 ECharts 内部强调状态,避免画布残留单条资金边高亮。
- 空白点击监听仅绑定资金流向图谱画布,不影响下方逐笔流水表格和分页操作。
- 逐笔流水接口增加请求序号校验,返回全量后丢弃旧选中边的异步响应,避免旧请求回写导致明细区再次出现。
- 弹窗资金流向明细隐藏累计金额、交易笔数、关系三张汇总卡片,减少重复信息占用空间。
## 影响范围
- 前端页面:结果总览“查看详情”弹窗中的“资金流向”页签。
- 复用组件:`FundGraphSection` 新增弹窗场景可配置项,默认行为不变。
- 不影响专项排查页图谱展示,不修改后端接口和数据库。
## 验证情况
- 前端命令执行前已尝试 `nvm use`,当前环境未识别 `nvm`;实际 Node 版本为 `v22.22.0`npm 版本为 `10.9.4`
- 已多次执行 `npm run build:prod`,构建通过,仅保留现有资源体积提示。
- 已使用真实页面验证 `http://localhost/ccdiProject/detail/90342?tab=overview`:在结果总览点击“查看详情”,切换“资金流向”,选中交易笔数最多的资金边。
- 多笔金额场景验证结果:当前图谱 19 条资金边,选中边为“彭静勇 → 张建强”,交易笔数 5逐笔流水表格显示 5 条并分页显示“共 5 条”。
- 展示验证结果:旧“弹窗速览”提示和“项目分析弹窗仅展示汇总信息”说明均不再出现;金额标签压缩为 `2.22万, 5笔`;汇总卡片已隐藏;逐笔流水展示在图谱下方,未裁切图谱区域。
- 恢复全量验证结果:选中“彭静勇 → 张建强”后展示 5 条逐笔流水,其余 18 条资金边降透明;点击图谱空白区域后,旧明细请求未回写,选中边、选中节点、逐笔流水数据和明细总数均清空;图表仍展示 15 个图形节点、19 条资金边,边样式恢复为 opacity `0.9`、width `1.9`,不再残留单条高亮边。
- 页面截图已保存到 `output/browser-use/project-analysis-fund-flow-final-graph.png``output/browser-use/project-analysis-fund-flow-final-detail.png``output/browser-use/project-analysis-fund-flow-reset-all.png``output/browser-use/project-analysis-fund-flow-reset-first-view.png``output/browser-use/project-analysis-fund-flow-blank-click-reset.png`

View File

@@ -1,5 +1,5 @@
<template>
<div class="project-analysis-fund-graph">
<div class="project-analysis-fund-graph" :class="rootClasses">
<fund-graph-section
ref="fundGraphSection"
:initial-keyword="defaultKeyword"
@@ -7,8 +7,12 @@
:auto-scroll="false"
:hide-header="true"
:graph-tabs="graphTabs"
:layout-scale="0.94"
:show-edge-detail-table="false"
:layout-scale="1.03"
:show-edge-detail-table="true"
:edge-detail-table-height="220"
compact-graph-labels
:show-edge-metrics="!isFundOnly"
:stacked-fund-detail-pane="isFundOnly"
hide-empty-detail-pane
/>
</div>
@@ -46,6 +50,15 @@ export default {
},
},
computed: {
isFundOnly() {
return this.initialGraphTab === "fund" && this.graphTabs.length === 2;
},
rootClasses() {
return {
"project-analysis-fund-graph--fund": this.isFundOnly,
"project-analysis-fund-graph--relation": !this.isFundOnly,
};
},
defaultKeyword() {
return (this.modelSummary && this.modelSummary.staffIdCard)
|| (this.person && (this.person.idNo || this.person.staffIdCard || this.person.staffName || this.person.name))
@@ -76,7 +89,7 @@ export default {
<style lang="scss" scoped>
.project-analysis-fund-graph {
min-height: 520px;
min-height: 780px;
::v-deep .graph-analysis-section {
margin-top: 0;
@@ -84,28 +97,44 @@ export default {
}
::v-deep .fund-workbench {
grid-template-columns: minmax(0, 1fr) 316px;
max-width: 1000px;
min-height: 450px;
grid-template-columns: minmax(0, 1fr);
max-width: 100%;
min-height: 760px;
}
::v-deep .fund-workbench.fund-workbench--single {
grid-template-columns: minmax(0, 1fr);
max-width: 760px;
max-width: 100%;
min-height: 560px;
}
::v-deep .fund-workbench--stacked {
display: flex;
flex-direction: column;
}
::v-deep .fund-left-pane {
padding: 10px 12px 12px;
padding: 14px 16px 16px;
overflow: hidden;
border-right: 0;
}
::v-deep .fund-right-pane {
padding: 12px;
position: relative;
z-index: 2;
padding: 14px;
background: #ffffff;
border-top: 1px solid #dce7f2;
overflow: hidden;
}
::v-deep .graph-canvas-wrap,
::v-deep .detail-empty {
::v-deep .graph-canvas-wrap {
height: auto;
min-height: 360px;
min-height: 520px;
}
::v-deep .detail-empty {
min-height: 240px;
}
::v-deep .query-row {
@@ -124,6 +153,21 @@ export default {
font-size: 12px;
}
::v-deep .detail-table .el-table__cell {
padding: 5px 0;
}
::v-deep .pagination-container {
margin-top: 8px;
padding: 0;
}
::v-deep .pagination-container .el-pagination {
display: flex;
justify-content: flex-end;
white-space: nowrap;
}
::v-deep .query-row {
gap: 8px;
margin-bottom: 10px;
@@ -131,32 +175,73 @@ export default {
::v-deep .graph-shell {
border-radius: 10px;
overflow: hidden;
}
::v-deep .graph-canvas {
min-height: 360px;
min-height: 520px;
}
::v-deep .detail-title {
font-size: 15px;
line-height: 1.35;
}
::v-deep .node-field-row,
::v-deep .edge-metrics span,
::v-deep .edge-metrics strong,
::v-deep .detail-subtitle {
font-size: 11px;
font-size: 12px;
}
}
.project-analysis-fund-graph--relation {
min-height: 520px;
::v-deep .fund-workbench {
grid-template-columns: minmax(0, 1fr);
max-width: 760px;
min-height: 450px;
}
::v-deep .graph-canvas-wrap,
::v-deep .detail-empty {
min-height: 360px;
}
::v-deep .graph-canvas {
min-height: 360px;
}
}
@media (max-width: 1440px) {
.project-analysis-fund-graph {
.project-analysis-fund-graph--fund {
::v-deep .fund-workbench {
grid-template-columns: minmax(0, 1fr) 300px;
max-width: 940px;
grid-template-columns: minmax(0, 1fr);
max-width: 100%;
}
::v-deep .fund-workbench.fund-workbench--single {
grid-template-columns: minmax(0, 1fr);
max-width: 100%;
}
::v-deep .graph-canvas-wrap {
min-height: 500px;
}
::v-deep .detail-empty {
min-height: 220px;
}
::v-deep .graph-canvas {
min-height: 500px;
}
}
.project-analysis-fund-graph--relation {
::v-deep .fund-workbench {
grid-template-columns: minmax(0, 1fr);
max-width: 720px;
}

View File

@@ -37,7 +37,10 @@
<div
v-show="activeGraphTab === 'fund'"
class="fund-workbench"
:class="{ 'fund-workbench--single': shouldHideFundDetailPane }"
:class="{
'fund-workbench--single': shouldHideFundDetailPane,
'fund-workbench--stacked': stackedFundDetailPane,
}"
>
<div class="fund-left-pane">
<div class="query-row">
@@ -155,7 +158,7 @@
</div>
</div>
<div class="edge-metrics">
<div v-if="showEdgeMetrics" class="edge-metrics">
<div>
<span>累计金额</span>
<strong>{{ formatMoney(selectedEdge.totalAmount) }}</strong>
@@ -199,7 +202,7 @@
v-loading="detailLoading"
:data="edgeDetails"
size="mini"
height="342"
:height="edgeDetailTableHeight"
class="detail-table"
>
<template slot="empty">
@@ -512,6 +515,22 @@ export default {
type: Boolean,
default: true,
},
edgeDetailTableHeight: {
type: Number,
default: 342,
},
compactGraphLabels: {
type: Boolean,
default: false,
},
showEdgeMetrics: {
type: Boolean,
default: true,
},
stackedFundDetailPane: {
type: Boolean,
default: false,
},
hideEmptyDetailPane: {
type: Boolean,
default: false,
@@ -521,6 +540,7 @@ export default {
return {
activeGraphTab: this.initialGraphTab,
chart: null,
chartBlankClickHandler: null,
relationChart: null,
graphLoading: false,
relationGraphLoading: false,
@@ -532,6 +552,7 @@ export default {
selectedRelationNode: null,
edgeDetails: [],
detailTotal: 0,
edgeDetailRequestSeq: 0,
expandedObjectKeys: [],
contextMenu: {
visible: false,
@@ -707,8 +728,7 @@ export default {
this.resizeRelationChart();
});
} else if (this.chart) {
this.chart.dispose();
this.chart = null;
this.destroyFundChart();
}
},
initialKeyword: {
@@ -733,10 +753,7 @@ export default {
window.removeEventListener("resize", this.resizeChart);
window.removeEventListener("resize", this.resizeRelationChart);
document.removeEventListener("click", this.hideContextMenu);
if (this.chart) {
this.chart.dispose();
this.chart = null;
}
this.destroyFundChart();
if (this.relationChart) {
this.relationChart.dispose();
this.relationChart = null;
@@ -1001,6 +1018,17 @@ export default {
}
if (!this.chart) {
this.chart = echarts.init(this.$refs.chart);
this.chartBlankClickHandler = (event) => {
if (event && event.target) {
return;
}
if (!this.selectedNode && !this.selectedEdge) {
return;
}
this.hideContextMenu();
this.resetFundSelection();
};
this.chart.getZr().on("click", this.chartBlankClickHandler);
this.chart.on("click", (params) => {
this.hideContextMenu();
if (params.dataType === "edge" && params.data && params.data.raw) {
@@ -1034,6 +1062,18 @@ export default {
}
this.renderChart();
},
destroyFundChart() {
if (!this.chart) {
this.chartBlankClickHandler = null;
return;
}
if (this.chartBlankClickHandler && this.chart.getZr) {
this.chart.getZr().off("click", this.chartBlankClickHandler);
}
this.chartBlankClickHandler = null;
this.chart.dispose();
this.chart = null;
},
initRelationChart() {
if (!this.$refs.relationChart) {
return;
@@ -1212,23 +1252,35 @@ export default {
}],
}, true);
},
renderChart() {
renderChart(options = {}) {
if (!this.chart) {
return;
}
const resetViewport = !!options.resetViewport;
this.syncChartSize();
if (resetViewport) {
this.chart.clear();
}
const centerObjectKey = this.graph.centerNode && this.graph.centerNode.objectKey;
const rawNodes = this.graph.nodes || [];
const rawEdges = this.graph.edges || [];
const chartWidth = this.chart.getWidth ? this.chart.getWidth() : 900;
const chartHeight = this.chart.getHeight ? this.chart.getHeight() : 540;
const nodeLayout = this.buildNodeLayout(rawNodes, rawEdges, centerObjectKey, chartWidth, chartHeight);
const selectedEdge = this.selectedEdge;
const hasFundSelection = !!(this.selectedNode || selectedEdge);
const nodes = rawNodes.map((node) => {
const isCenter = node.objectKey === centerObjectKey;
const isFamily = !!node.relationType;
const isExpanded = this.expandedObjectKeys.includes(node.objectKey);
const category = isCenter ? 0 : isFamily ? 1 : isExpanded ? 3 : 2;
const layout = nodeLayout.get(node.nodeKey) || {};
const displayName = this.compactGraphLabels
? this.truncateGraphText(node.nodeName, isCenter ? 8 : 7)
: (node.nodeName || "-");
const isSelectedNode = this.selectedNode && this.selectedNode.objectKey === node.objectKey;
const inSelectedEdge = this.isNodeInSelectedEdge(node, selectedEdge);
const shouldDim = selectedEdge && !inSelectedEdge;
return {
id: node.nodeKey,
name: node.nodeName,
@@ -1240,7 +1292,7 @@ export default {
fixed: true,
category,
raw: node,
itemStyle: this.getNodeStyle(category, this.selectedNode && this.selectedNode.objectKey === node.objectKey),
itemStyle: this.getNodeStyle(category, isSelectedNode || inSelectedEdge, shouldDim),
emphasis: {
scale: isCenter ? 1.08 : 1.12,
itemStyle: {
@@ -1257,13 +1309,13 @@ export default {
show: true,
position: "bottom",
distance: isCenter ? 8 : 6,
color: "#1b1f2a",
color: shouldDim ? "#8794a6" : "#1b1f2a",
fontSize: isCenter ? 14 : 13,
fontWeight: isCenter ? 700 : 500,
formatter: isFamily ? `{name|${node.nodeName || "-"} }\n{tag|${node.relationType}}` : "{b}",
formatter: isFamily ? `{name|${displayName} }\n{tag|${node.relationType}}` : displayName,
rich: {
name: {
color: "#1b1f2a",
color: shouldDim ? "#8794a6" : "#1b1f2a",
fontWeight: isCenter ? 700 : 500,
fontSize: isCenter ? 14 : 13,
lineHeight: 17,
@@ -1280,6 +1332,8 @@ export default {
const edgeCurves = this.buildEdgeCurves(rawEdges);
const edges = rawEdges.map((edge, index) => {
const isOut = String(edge.direction) === "1";
const selected = this.isSelectedEdge(edge, selectedEdge);
const shouldDim = selectedEdge && !selected;
return {
source: edge.fromKey,
target: edge.toKey,
@@ -1293,8 +1347,8 @@ export default {
},
lineStyle: {
color: this.isExpandedEdge(edge) ? (isOut ? "#4f78ab" : "#3a8f83") : (isOut ? "#245f95" : "#1c7f73"),
opacity: this.isExpandedEdge(edge) ? 0.72 : 0.9,
width: this.isExpandedEdge(edge) ? 1.5 : 1.9,
opacity: shouldDim ? 0.22 : selected ? 1 : this.isExpandedEdge(edge) ? 0.72 : 0.9,
width: selected ? 2.8 : this.isExpandedEdge(edge) ? 1.5 : 1.9,
curveness: edgeCurves[index],
type: this.isManualEdge(edge) ? "dashed" : "solid",
},
@@ -1364,23 +1418,23 @@ export default {
edgeLabel: {
position: "middle",
rotate: 0,
distance: 22,
color: "#4b5a6a",
fontSize: 11,
distance: this.compactGraphLabels ? 18 : 22,
color: selectedEdge ? "#516070" : "#4b5a6a",
fontSize: this.compactGraphLabels ? 10 : 11,
fontWeight: 500,
fontFamily: "'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif",
backgroundColor: "rgba(255,255,255,0.92)",
backgroundColor: this.compactGraphLabels ? "rgba(255,255,255,0.86)" : "rgba(255,255,255,0.92)",
borderColor: "rgba(205, 216, 228, 0.92)",
borderWidth: 1,
borderRadius: 8,
padding: [3, 7],
padding: this.compactGraphLabels ? [2, 5] : [3, 7],
},
edgeLabelLayout: (params) => this.layoutEdgeLabel(params, nodeLayout, rawEdges, edgeCurves),
labelLayout: { hideOverlap: false },
data: nodes.concat(boundaryNodes),
links: edges,
emphasis: {
focus: "adjacency",
focus: hasFundSelection ? "none" : "adjacency",
scale: true,
lineStyle: {
opacity: 0.92,
@@ -1389,7 +1443,9 @@ export default {
},
}],
}, true);
this.resizeChart();
if (!resetViewport) {
this.resizeChart();
}
},
buildBoundaryNodes(chartWidth, chartHeight) {
const padding = 56;
@@ -1631,10 +1687,10 @@ export default {
}
return PROXY_SYMBOL;
},
getNodeStyle(category, selected) {
getNodeStyle(category, selected, dimmed = false) {
const isCenter = category === 0;
return {
opacity: category === 3 ? 0.88 : 1,
opacity: dimmed ? 0.42 : category === 3 ? 0.88 : 1,
shadowBlur: selected ? 16 : isCenter ? 10 : 6,
shadowOffsetY: selected ? 4 : 2,
shadowColor: selected ? "rgba(42, 111, 174, 0.24)" : "rgba(45, 84, 124, 0.12)",
@@ -1644,6 +1700,7 @@ export default {
this.selectedEdge = edge;
this.selectedNode = null;
this.detailQuery.pageNum = 1;
this.renderChart();
if (this.isManualEdge(edge)) {
this.edgeDetails = [];
this.detailTotal = 0;
@@ -1655,7 +1712,9 @@ export default {
if (!this.selectedEdge) {
return;
}
if (!this.selectedEdge.fromKey || !this.selectedEdge.toKey) {
const requestSeq = ++this.edgeDetailRequestSeq;
const selectedEdge = this.selectedEdge;
if (!selectedEdge.fromKey || !selectedEdge.toKey) {
this.edgeDetails = [];
this.detailTotal = 0;
this.$message.warning("当前边缺少明细查询参数");
@@ -1664,26 +1723,34 @@ export default {
this.detailLoading = true;
try {
const response = await getFundGraphEdgeDetail({
fromKey: this.selectedEdge.fromKey,
toKey: this.selectedEdge.toKey,
direction: this.selectedEdge.direction,
fromKey: selectedEdge.fromKey,
toKey: selectedEdge.toKey,
direction: selectedEdge.direction,
transactionStartTime: this.query.transactionStartTime,
transactionEndTime: this.query.transactionEndTime,
pageNum: this.detailQuery.pageNum,
pageSize: this.detailQuery.pageSize,
});
if (requestSeq !== this.edgeDetailRequestSeq || this.selectedEdge !== selectedEdge) {
return;
}
this.edgeDetails = response.rows || [];
this.detailTotal = response.total || 0;
} catch (error) {
if (requestSeq !== this.edgeDetailRequestSeq || this.selectedEdge !== selectedEdge) {
return;
}
this.edgeDetails = [];
this.detailTotal = 0;
this.$message.error("流水明细查询失败");
console.error("流水明细查询失败", {
error,
selectedEdge: this.selectedEdge,
selectedEdge,
});
} finally {
this.detailLoading = false;
if (requestSeq === this.edgeDetailRequestSeq && this.selectedEdge === selectedEdge) {
this.detailLoading = false;
}
}
},
mergeGraph(currentGraph, nextGraph) {
@@ -1711,6 +1778,26 @@ export default {
isManualEdge(edge) {
return !!(edge && edge.sourceType === "MANUAL");
},
isSelectedEdge(edge, selectedEdge) {
if (!edge || !selectedEdge) {
return false;
}
if (edge.edgeKey && selectedEdge.edgeKey) {
return edge.edgeKey === selectedEdge.edgeKey;
}
return edge.fromKey === selectedEdge.fromKey
&& edge.toKey === selectedEdge.toKey
&& String(edge.direction || "") === String(selectedEdge.direction || "");
},
isNodeInSelectedEdge(node, selectedEdge) {
if (!node || !selectedEdge) {
return false;
}
return node.nodeKey === selectedEdge.fromKey
|| node.nodeKey === selectedEdge.toKey
|| node.objectKey === selectedEdge.fromObjectKey
|| node.objectKey === selectedEdge.toObjectKey;
},
buildRelationNodeLayout(nodes, edges, centerObjectKey, chartWidth, chartHeight) {
const layout = new Map();
if (!nodes.length) {
@@ -1860,10 +1947,30 @@ export default {
this.queryByNode(node);
},
clearSelection() {
this.edgeDetailRequestSeq += 1;
this.selectedNode = null;
this.selectedEdge = null;
this.edgeDetails = [];
this.detailTotal = 0;
this.detailLoading = false;
},
resetFundSelection() {
this.clearChartEmphasis();
this.clearSelection();
this.renderChart({ resetViewport: true });
this.$nextTick(() => {
this.clearChartEmphasis();
});
},
clearChartEmphasis() {
if (!this.chart) {
return;
}
this.chart.dispatchAction({ type: "hideTip" });
this.chart.dispatchAction({
type: "downplay",
seriesIndex: 0,
});
},
resizeChart() {
this.$nextTick(() => {
@@ -1922,8 +2029,29 @@ export default {
formatEdgeLabel(edge) {
const amount = Number(edge && edge.totalAmount ? edge.totalAmount : 0);
const count = Number(edge && edge.transactionCount ? edge.transactionCount : 0);
if (this.compactGraphLabels) {
return `${this.formatCompactMoney(amount)}, ${count}`;
}
return `${amount.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}元, ${count}`;
},
formatCompactMoney(value) {
const number = Math.abs(Number(value || 0));
const sign = Number(value || 0) < 0 ? "-" : "";
if (number >= 100000000) {
return `${sign}${(number / 100000000).toLocaleString("zh-CN", { maximumFractionDigits: 2 })}亿`;
}
if (number >= 10000) {
return `${sign}${(number / 10000).toLocaleString("zh-CN", { maximumFractionDigits: 2 })}万`;
}
return `${sign}${number.toLocaleString("zh-CN", { maximumFractionDigits: 0 })}元`;
},
truncateGraphText(value, maxLength) {
const text = String(value || "-");
if (text.length <= maxLength) {
return text;
}
return `${text.slice(0, Math.max(1, maxLength - 1))}`;
},
formatShortMoney(value) {
const number = Number(value || 0);
return `${number.toLocaleString("zh-CN", { maximumFractionDigits: 2 })}元`;