前端怎么实现一个利润表?

前端怎么实现一个下面的图,这个图是zebrabi里面的,但是不开源

image.png

给一些思路和建议

阅读 1k
avatarAI BotBETA

利润表实现方案

  1. 数据结构设计
  2. 使用树形结构数据表示科目层级关系(如:收入->主营业务收入)
  3. 每个节点包含:科目名称、本期数、累计数、占比等字段
  4. 技术选型
  5. 表格组件:ag-Grid Enterprise(支持分组/树形展示)
  6. 计算引擎:math.js/formula.js 处理跨行计算
  7. 数据绑定:Vue/React响应式数据驱动
  8. 核心功能实现

    // 动态计算公式示例
    const formulas = {
      '利润总额': (data) => data.营业收入 - data.营业成本,
      '净利润': (data) => data.利润总额 - data.所得税费用
    }

可视化图表实现思路

  1. 图表类型分析
  2. 组合图表:柱状图(金额指标)+ 折线图(比率指标)
  3. 辅助元素:趋势线、数据标注点、阈值参考线
  4. 关键技术方案

    // ECharts 多轴配置示例
    option = {
      xAxis: { type: 'category' },
      yAxis: [
     { type: 'value', name: '金额' },
     { type: 'value', name: '比率', axisLabel: { formatter: '{value}%' }}
      ],
      series: [
     { type: 'bar', yAxisIndex: 0 },
     { type: 'line', yAxisIndex: 1 }
      ]
    }
  5. 高级功能实现
  6. 动态数据更新:WebSocket + 数据差异对比算法
  7. 交互式下钻:点击事件绑定+层级数据加载
  8. 可视化配置:通过JSON Schema生成配置面板

性能优化建议

  1. 大数据量采用虚拟滚动技术
  2. 使用Web Worker处理财务计算
  3. 画布渲染优化:
  4. 分层渲染(背景层/数据层/交互层)
  5. 增量渲染(只重绘变化部分)
  6. 内存管理:
  7. 对象复用池
  8. 大数据分块加载

扩展能力设计

  1. 插件式架构设计
  2. 公式解析引擎支持自定义函数
  3. 主题系统(CSS变量+样式注入)
  4. 多语言支持(i18n标准方案)

安全注意事项

  1. 财务数据加密传输(Web Crypto API)
  2. XSS防护:禁用动态HTML插入
  3. 操作审计日志记录
  4. 细粒度权限控制(CASL实现)
2 个回答

由于 ZebraBI 不开源,可以考虑使用以下开源图表库,帮助开发:

ECharts:
功能丰富,支持多种图表类型,包括柱状图、折线图等。可以通过配置项灵活定制图表样式、交互等。例如,对于利润表中的各项数据对比,可以使用柱状图展示实际值、预算值等,通过设置不同颜色区分。
Highcharts:
具有良好的兼容性和交互性,提供了丰富的 API 来实现图表的定制和交互功能。它对于财务数据的可视化有较好的支持,能方便地添加数据标签、趋势线等元素。
D3.js:
灵活性高,可基于数据驱动进行各种图表的绘制。虽然上手难度相对较大,但对于复杂图表的定制能力很强。可以根据利润表的数据特点,自定义绘制图表元素和交互逻辑。

这下面两个都可以:其他的自定义

    • React + Ant Design/Element UI
  • Vue + Element UI/Ant Design Vue

    import React from 'react';
    import { Table, Dropdown, Menu, Button } from 'antd';
    import { DownOutlined } from '@ant-design/icons';
    import './ProfitTable.css';
    
    const ProfitTable = () => {
    const actionMenu = (
      <Menu>
        <Menu.Item key="1">下钻</Menu.Item>
        <Menu.Item key="2">上钻</Menu.Item>
        <Menu.Item key="3">指标归因</Menu.Item>
        <Menu.Item key="4">多维归因</Menu.Item>
        <Menu.Item key="5">洞察</Menu.Item>
      </Menu>
    );
    
    const renderDiffBar = (value) => (
      <div className="diff-bar-container">
        <span className="diff-value">{value > 0 ? `+${value}` : value}</span>
        <div 
          className={`diff-bar ${value > 0 ? 'positive' : 'negative'}`}
          style={{ 
            width: `${Math.min(Math.abs(value) * 3, 100)}px`,
            marginLeft: value < 0 ? 'auto' : '0'
          }}
        />
      </div>
    );
    
    const columns = [
      {
        title: '',
        dataIndex: 'category',
        key: 'category',
        width: 100,
        fixed: 'left',
      },
      {
        title: '实际',
        dataIndex: 'actual',
        key: 'actual',
        width: 80,
        align: 'right',
      },
      {
        title: '预算',
        dataIndex: 'budget',
        key: 'budget',
        width: 80,
        align: 'right',
      },
      {
        title: '同期',
        dataIndex: 'sameperiod',
        key: 'sameperiod',
        width: 80,
        align: 'right',
      },
      {
        title: 'Δ预算',
        dataIndex: 'budgetDiff',
        key: 'budgetDiff',
        width: 200,
        render: (text) => renderDiffBar(text),
      },
      {
        title: 'Δ预算%',
        dataIndex: 'budgetDiffPercent',
        key: 'budgetDiffPercent',
        width: 120,
        render: (text) => <span>{text > 0 ? `+${text}` : text}</span>,
      },
      {
        title: 'ΔPY%',
        dataIndex: 'pyDiffPercent',
        key: 'pyDiffPercent',
        width: 120,
        render: (text) => <span>{text > 0 ? `+${text}` : text}</span>,
      },
      {
        title: '操作',
        key: 'action',
        width: 80,
        render: () => (
          <Dropdown overlay={actionMenu}>
            <Button size="small">
              <DownOutlined />
            </Button>
          </Dropdown>
        ),
      },
    ];
    
    const data = [
      {
        key: '1',
        category: '能源电力',
        actual: 136.0,
        budget: 111.8,
        sameperiod: 88.3,
        budgetDiff: 24.3,
        budgetDiffPercent: 21.7,
        pyDiffPercent: 54.0,
      },
      {
        key: '2',
        category: '装备制造',
        actual: 17.3,
        budget: 53.2,
        sameperiod: 36.7,
        budgetDiff: -35.9,
        budgetDiffPercent: -67.5,
        pyDiffPercent: -52.9,
      },
      // 添加更多数据...
    ];
    
    return (
      <div className="profit-table-container">
        <Table 
          columns={columns} 
          dataSource={data} 
          pagination={false}
          bordered
          size="middle"
          scroll={{ x: 'max-content' }}
        />
      </div>
    );
    };
    
    export default ProfitTable;
    
    /* ProfitTable.css */
    .profit-table-container {
    margin: 20px;
    }
    
    .diff-bar-container {
    display: flex;
    align-items: center;
    position: relative;
    height: 24px;
    width: 100%;
    }
    
    .diff-value {
    position: absolute;
    z-index: 2;
    font-weight: bold;
    }
    
    .diff-bar {
    height: 16px;
    border-radius: 2px;
    }
    
    .diff-bar.positive {
    background-color: #8BC34A;
    }
    
    .diff-bar.negative {
    background-color: #F44336;
    }
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题