Vue3 + AntV G6 实战:手把手教你绘制可折叠的财务科目生态图
Vue3 + AntV G6 实战:构建智能财务科目可视化系统
在当今数据驱动的商业环境中,财务数据的可视化呈现已成为企业决策的重要支撑。传统表格展示方式难以直观反映复杂的科目层级关系,而基于Vue3和AntV G6的技术组合,能够打造出交互丰富、视觉直观的财务科目生态图。本文将深入探讨如何利用这套技术栈,构建一个支持动态折叠、金额展示和智能布局的专业级财务可视化解决方案。
1. 环境搭建与基础配置
1.1 创建Vue3项目与安装依赖
现代前端开发中,Vite已成为构建工具的首选。我们首先创建一个基于Vite的Vue3项目:
npm create vite@latest financial-visualization --template vue-ts cd financial-visualization npm install @antv/g6 vue-demi关键依赖说明:
@antv/g6: AntV系列的专业图可视化引擎vue-demi: 帮助库兼容Vue2/Vue3的辅助工具
1.2 初始化G6图表容器
在Vue组件中,我们需要准备图表渲染的DOM容器:
<template> <div class="visualization-container"> <div ref="graphContainer" class="graph-wrapper"></div> </div> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue' const graphContainer = ref<HTMLDivElement>() </script> <style scoped> .graph-wrapper { width: 100%; height: 600px; border: 1px solid #e8e8e8; border-radius: 4px; } </style>2. 财务科目数据结构设计
2.1 符合会计标准的树形结构
财务科目数据通常呈现严格的层级关系,以下是一个符合会计准则的数据结构示例:
interface FinancialNode { id: string code: string // 科目编码 name: string // 科目名称 amount: number // 科目金额 level: number // 科目层级 isLeaf?: boolean // 是否末级科目 children?: FinancialNode[] } const mockData: FinancialNode = { id: 'root', code: '1001', name: '资产类', amount: 125000000, level: 0, children: [ { id: 'c1', code: '100101', name: '流动资产', amount: 75000000, level: 1, children: [ { id: 'c11', code: '10010101', name: '货币资金', amount: 50000000, level: 2, isLeaf: false } ] } ] }2.2 数据转换与预处理
实际业务中,数据可能需要从后端API获取并进行转换:
const transformApiData = (apiData: any): FinancialNode => { return { id: apiData.accountId, code: apiData.accountCode, name: apiData.accountName, amount: apiData.balance, level: apiData.level, children: apiData.children?.map(transformApiData) } }3. 高级图表配置与自定义节点
3.1 注册专业财务节点类型
G6的强大之处在于允许完全自定义节点样式:
const registerFinancialNode = () => { G6.registerNode('financial-node', { draw(cfg, group) { const { name, amount, level, collapsed } = cfg const width = 240 const height = 60 // 基础矩形 const rect = group.addShape('rect', { attrs: { x: -width/2, y: -height/2, width, height, fill: getLevelColor(level), radius: 4, shadowColor: 'rgba(0,0,0,0.1)', shadowBlur: 6 } }) // 科目名称 group.addShape('text', { attrs: { text: name, x: -width/2 + 15, y: -10, fontSize: 14, fontWeight: 'bold', fill: '#333' } }) // 金额显示 group.addShape('text', { attrs: { text: formatAmount(amount), x: width/2 - 15, y: 15, fontSize: 12, textAlign: 'right', fill: amount < 0 ? '#f5222d' : '#52c41a' } }) // 折叠按钮 if(cfg.children?.length) { addCollapseButton(group, width, height, collapsed) } return rect } }) } const getLevelColor = (level: number) => { const colors = ['#f6ffed', '#e6f7ff', '#fff2e8', '#f9f0ff'] return colors[level % colors.length] } const formatAmount = (amount: number) => { return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY', minimumFractionDigits: 2 }).format(amount) }3.2 响应式布局配置
针对财务科目特点优化树形布局:
const getLayoutConfig = () => ({ type: 'dendrogram', direction: 'TB', nodeSep: 40, rankSep: 100, radial: false })4. Vue3与G6深度集成实践
4.1 组合式API封装图表逻辑
利用Vue3的Composition API封装可复用的图表逻辑:
import { ref, onMounted, onUnmounted, watch } from 'vue' export function useFinancialGraph(containerRef: Ref<HTMLDivElement>, initialData: FinancialNode) { const graph = ref<G6.TreeGraph>() const currentData = ref<FinancialNode>(initialData) const initGraph = () => { if (!containerRef.value) return registerFinancialNode() graph.value = new G6.TreeGraph({ container: containerRef.value, width: containerRef.value.clientWidth, height: containerRef.value.clientHeight, modes: { default: ['drag-canvas', 'zoom-canvas'] }, defaultNode: { type: 'financial-node' }, layout: getLayoutConfig() }) graph.value.data(currentData.value) graph.value.render() graph.value.fitView() } const handleResize = () => { if (graph.value && containerRef.value) { graph.value.changeSize( containerRef.value.clientWidth, containerRef.value.clientHeight ) graph.value.fitView() } } onMounted(() => { initGraph() window.addEventListener('resize', handleResize) }) onUnmounted(() => { window.removeEventListener('resize', handleResize) graph.value?.destroy() }) watch(currentData, (newData) => { if (graph.value) { graph.value.changeData(newData) graph.value.fitView() } }) return { graph, currentData } }4.2 业务组件集成示例
在业务组件中使用封装好的图表逻辑:
<script setup lang="ts"> import { ref } from 'vue' import { useFinancialGraph } from './useFinancialGraph' import { fetchFinancialData } from './api' const containerRef = ref<HTMLDivElement>() const { currentData } = useFinancialGraph(containerRef, {}) // 加载数据 const loadData = async () => { const res = await fetchFinancialData() currentData.value = res.data } </script> <template> <div class="financial-dashboard"> <div ref="containerRef" class="graph-container"></div> <button @click="loadData">刷新数据</button> </div> </template>5. 性能优化与高级功能
5.1 大数据量优化策略
当处理大型企业财务数据时,需要考虑性能优化:
const optimizeLargeData = (graph: G6.TreeGraph) => { // 1. 启用虚拟渲染 graph.get('canvas').set('localRefresh', false) // 2. 分级加载 graph.on('collapse-text:click', (e) => { const item = e.item if(item.getModel().collapsed) { loadChildrenData(item.getModel().id).then(children => { item.getModel().children = children graph.refreshItem(item) }) } }) // 3. 简化非活跃节点 graph.on('viewportchange', () => { const nodes = graph.getNodes() nodes.forEach(node => { const bbox = node.getBBox() const viewCenter = graph.getPointByCanvas( graph.getWidth()/2, graph.getHeight()/2 ) const distance = Math.sqrt( Math.pow(bbox.centerX - viewCenter.x, 2) + Math.pow(bbox.centerY - viewCenter.y, 2) ) if(distance > 800) { node.getContainer().hide() } else { node.getContainer().show() } }) }) }5.2 交互增强功能
为财务图表添加专业交互功能:
const addInteractions = (graph: G6.TreeGraph) => { // 金额汇总高亮 graph.on('node:mouseenter', (e) => { const node = e.item const amount = node.getModel().amount graph.getNodes().forEach(n => { if(n.getModel().amount > amount * 10) { n.getContainer().set('highlight', true) } }) }) // 右键菜单 graph.on('node:contextmenu', (e) => { e.preventDefault() showContextMenu(e.item.getModel()) }) // 快捷键支持 document.addEventListener('keydown', (e) => { if(e.key === 'Escape') { graph.fitView() } }) }6. 企业级应用扩展
6.1 多视图协同分析
构建复杂的财务分析仪表盘:
<template> <div class="financial-analysis"> <div class="main-graph"> <FinancialGraph :data="mainData" /> </div> <div class="detail-panel"> <AccountDetail v-if="selectedNode" :node="selectedNode" /> <TrendChart :data="trendData" /> </div> </div> </template> <script setup> const selectedNode = ref(null) const handleNodeClick = (node) => { selectedNode.value = node fetchTrendData(node.id).then(data => { trendData.value = data }) } </script>6.2 审计追踪功能
为财务可视化添加变更记录:
const addAuditTrail = (graph: G6.TreeGraph) => { const history = [] graph.on('afterupdateitem', (e) => { history.push({ timestamp: new Date(), nodeId: e.item.getID(), action: 'update', before: e.cfg.before, after: e.item.getModel() }) }) graph.on('afteradditem', (e) => { history.push({ timestamp: new Date(), nodeId: e.item.getID(), action: 'add', data: e.item.getModel() }) }) return { history, undo: () => { const lastAction = history.pop() if(lastAction) { // 实现撤销逻辑 } } } }