V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
GrapeCityChina
V2EX  ›  推广

Redux 与前端表格施展“组合拳”,实现大屏展示应用的交互增强

  •  
  •   GrapeCityChina · 2023-01-11 14:13:23 +08:00 · 505 次点击
    这是一个创建于 461 天前的主题,其中的信息可能已经有所发展或是发生改变。

    Redux 是 JavaScript 状态容器,提供可预测化的状态管理。它可以用在 react 、angular 、vue 等项目中, 但与 react 配合使用更加方便一些。

    Redux 原理图如下,可以看到 store 仓库是 Redux 的核心,通过维护一个 store 仓库管理 state 。state 是只读的,唯一改变 state 的方法就是组件触发 Action 。通过编写 Reducers 函数,它会接收先前的 state 和 action ,并返回新的 state 。

    Redux 的核心理念就是如何根据这些 action 对象来更新 state ,强制使用 action 来描述所有变化带来的好处是你可以清晰地知道应用中到底发生了什么。如果一些东西改变了,你可以知道为什么变化,action 就是描述发生了什么的指示器。

    来看一下 Redux 在大屏展示中具体的使用场景:

    下面的截图是一个产品开发中非常常见的大屏展示界面示例。核心的数据源为一组销售数据,上方三个仪表板以及下方的表格组件共享同一个数据源,实现了数据明细显示以及各维度的数据统计。

    从图上来看,似乎已经具备了大屏展示的数据显示和统计功能,但是展示的数据是没有办法被编辑和修改的。此时,你可能会收到来自客户的灵魂拷问:

    “展示功能已经不错了,但是表格数据可以实时编辑更新吗?”

    图中的销售明细数据是用 html 表格直接显示的,如果要实现编辑,通常的做法是,我们挑选一个前端表格组件,实现编辑的功能。

    文末可下载文章代码文件。

    将表格添加到你的 React 应用程序 我们要用电子表格替换这个 html 表格,修改 component 文件夹中的 SalesTable.js ,替换其中的 table 。

    <SpreadSheets hostClass={config.hostClass} workbookInitialized={workbookInit}  valueChanged={(e,info) => handleValueChanged(e,info)}>
    				<Worksheet name={config.sheetName} dataSource={tableData} autoGenerateColumns={config.autoGenerateColumns}  >
    					<Column width={50} dataField='id' headerText="编号"></Column>
    					<Column width={200} dataField='client' headerText="客户"></Column>
    					<Column width={320} dataField='description' headerText="描述"></Column>
    					<Column width={100} dataField='value' headerText="销售额" formatter={config.priceFormatter} resizable="resizable"></Column>
    					<Column width={100} dataField='itemCount' headerText="数量"></Column>
    					<Column width={100} dataField='soldBy' headerText="销售人员"></Column>
    					<Column width={100} dataField='country' headerText="国家"></Column>                   
    				</Worksheet>
    			</SpreadSheets>
    
    

    其中,SpreadSheets 元素创建了一个电子表格并定义了如何显示数据列。dataSource 属性定义了绑定的数据源,Column 中的 dataField 属性告诉该列应该绑定底层数据集的哪个属性。

    接下来是 js 代码部分,

    import '@grapecity/spread-sheets-react';
    import "@grapecity/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css";
    import { SpreadSheets, Worksheet, Column } from '@grapecity/spread-sheets-react';
    export const SalesTable = ({ tableData, valueChangedCallback,} ) => {
        const config = {
            sheetName: 'Sales Data',
            hostClass: ' spreadsheet',
            autoGenerateColumns: false,
            width: 200,
            visible: true,
            resizable: true,
            priceFormatter: '$ #.00',
            chartKey: 1
        }
    
    	function handleValueChanged(e, obj) {
    		valueChangedCallback(obj.sheet.getDataSource());
    	}
    	handleValueChanged.bind(this);
    
    	const [_spread, setSpread] = useState({});
    
    	function workbookInit(spread) { 
    		setSpread(spread) 
    	}
    	}
    
    

    只需很少的代码即可完成。config 中的几个数据属性。是绑定到电子表格中的组件的配置选项。workbookInit 方法是在初始化工作表时调用的回调。handleValueChanged 是在表格数据发生变化后的回调

    重新运行,即可显示电子表格数据:

    现在我们用一个完整的电子表格替换了原来的 html table ,此时可以对表格中的数据做任意的修改编辑,但是在编辑后上方的销售统计结果并不会实时更新,接下来我们就用 Redux 来创建一个 store 仓库用来存储销售数据,以实现数据的共享和实时更新。

    将 Redux 添加到你的 React 应用程序

    1.引入相关库

    "@reduxjs/toolkit": "^1.9.1",
    "react-redux": "^7.2.0",
    "redux": "^4.0.5"
    

    2.通过 createSlice 创建切片 新建一个 js 文件,写入下面的代码,通过 Redux 提供 createSlice 方法,我们创建了一个切片,初始化了 state ,在其中加入了销售明细数据作为 recentSales 。为 reducers 添加了两个方法 updatesales 和 importSales ,用于在销售明细数据更新或者导入这两种情况时,来同步 recentSales 。

    import {  createSlice } from '@reduxjs/toolkit';
    import { recentSalesdata } from "../data/data";
    const initialState = {
          recentSales: JSON.parse(JSON.stringify(recentSalesdata)),
          status: 'idle',
        };
    
    export const salesSlice = createSlice({
          name: 'recentSales',
          initialState,
          reducers: {
            importSales: (state,action) => {
              state.recentSales=JSON.parse(JSON.stringify(action.payload));
            },
            updatesales: (state,action) => {
                let sales=state.recentSales;
                let arr=sales.map(function(o){return o.id});
                console.log(arr);
                action.payload.forEach((newsale)=>{
                      if(arr.indexOf(newsale.id)>=0){
    
                        state.recentSales[arr.indexOf(newsale.id)]=JSON.parse(JSON.stringify(newsale));
                      }
                      else{
                        console.log("add");
                        state.recentSales.push(JSON.parse(JSON.stringify(newsale)));
                      }
                  });
              },
            
          },
    
        });
    export const { updatesales,importSales} = salesSlice.actions;
    export const recentSales = (state) =>  state.recentSales.recentSales;
    export default salesSlice.reducer;
    
    

    3.创建 store 添加 store.js 文件并加入下面的代码,这里创建的 store 中加入了刚刚创建的切片器。

    import { configureStore }  from '@reduxjs/toolkit';
    import recentSalesReducer from '../store/salesSlice';
    export const store = configureStore({
          reducer: {
                recentSales: recentSalesReducer,
          },
    });
    
    

    4.在 component 组件中使用 store 在 Dashboard.js 中,import 下面的代码。

    import { useSelector, useDispatch } from 'react-redux';
    import {
      updatesales,importSales, 
      recentSales
    } from '../store/salesSlice';
    
    

    然后在创建的 Dashboard 方法体中,再加入下面的代码,其中 react-redux 提供的:

    • useSelector 用于获取刚刚创建的 state 中的 recentSales 。
    • useDispatch 用于调用 reducer 中已经创建的方法来更新 recentSales 。
    const sales = useSelector(recentSales);
    const dispatch = useDispatch();
        function handleValueChanged(tableData) {
          dispatch(updatesales(tableData));
        }
        function handleFileImported(newSales) {
          dispatch(importSales(newSales));
        }
    
    

    对大屏展示面板加入 redux 做了上述改造后,就达到了销售数据编辑后,数据统计结果同步更新的效果:

    动图中可以看到上面三个仪表板显示的内容也同步进行了更新。原因是表格被编辑后,我们同步更新了 state 中的 recentSales 。

    好了,现在我们已经有了一个可以随着数据变化而实时更新的增强型仪表板。客户的需求顺利完成,但是在演示时,你很可能又会听到客户说出的下面的需求:

    “能支持 Excel 数据的导入导出吗?”

    如果您已经开发软件很长时间,您可能不止一次地从最终客户或者产品经理那里听到过这个灵魂拷问。对于非技术人群来说,觉得要求 Excel 导入 /导出 /展示是一个非常正常且容易实现的需求。

    但实际上,这个问题常常让前端开发人员感到束手无策。处理 Excel 文件需要大量工作。即使使用第三方的 grid 组件,也很难支持导入一个复杂的 Excel 表格作为数据。

    这个问题通过表格可以变得简单,导入和导入都可以直接实现。这也是我们在开始时使用将电子表格作为表格明细数据显示和编辑控件的原因。下面我们为应用加入 Excel 导入导出功能

    导出为 Excel 文件

    将 Excel 导入导出功能添加到工作表很容易。首先,在界面上添加相关的文件输入框和按钮。把它放在电子表格面板的底部,在 SpreadSheets 结束标记之后添加。

    	<div className="dashboardRow">
    				{/* EXPORT TO EXCEL */}
    				<button className="btn btn-primary dashboardButton" 
    					onClick={exportSheet}>Export to Excel</button>
    				{/* IMPORT FROM EXCEL */}
    				<div>
    					<b>Import Excel File:</b>
    					<div>
    						<input type="file" className="fileSelect" 
    						onChange={(e) => fileChange(e)} />
    					</div>
    				</div>
    			</div>
    
    

    接下来添加点击时触发的 exportSheet 方法,首先添加并导入下面的包,其中 @grapecity/spread-excelio 是 SpreadJS 中用于导入导出 Excel 的包。

    import { IO } from "@grapecity/spread-excelio";
    import { saveAs } from 'file-saver';
    
    

    然后将导出方法 exportSheet 添加到组件中:

    function exportSheet() {
    		const spread = _spread;
    		const fileName = "SalesData.xlsx";
    		const sheet = spread.getSheet(0);
    		const excelIO = new IO();
    		const json = JSON.stringify(spread.toJSON({ 
    			includeBindingSource: true,
    			columnHeadersAsFrozenRows: true,
    		}));
    		excelIO.save(json, (blob) => {
    			saveAs(blob, fileName);
    		}, function (e) {  
    			alert(e);  
    		});     
    	}
    
    

    运行测试点击按钮,即可直接获取到导出的 Excel 文件。 需要注意的是,我们设置了两个序列化选项:includeBindingSource 和 columnHeadersAsFrozenRows 。以确保绑定到工作表的数据被正确导出,且工作表包含列标题,

    Excel 数据导入

    我们继续来添加导入的方法,刚刚创建文件输入框,我们来处理它的 onChange 事件,创建一个 fileChange 方法

    	function fileChange(e) {
    		if (_spread) {
    			const fileDom = e.target || e.srcElement;
    			const excelIO = new IO();
    			const spread = _spread;
    			const deserializationOptions = {
    				frozenRowsAsColumnHeaders: true
    			};
    			excelIO.open(fileDom.files[0], (data) => {
    				const newSalesData = extractSheetData(data);
    				
    				spread.getSheet(0).setDataSource(newSalesData,false);
    				fileImportedCallback(newSalesData);
    			});
    		}
    	}
    
    

    选择文件后,使用 ExcelIO 导入它。获取其中的 json 数据。传入自定义的函数 extractSheetData ,从中提取需要的数据,然后设置给 SpreadJS 作为电子表格数据源,另外传给 fileImportedCallback 方法,这个函数中会调用 dispatch(importSales(newSales)); 来同步更新了 state 中的 recentSales 。

    extractSheetData 函数可以在 src/util.util.js 文件中找到,用于 解析 Excel 中的数据。extractSheetData 函数假定导入工作表中的数据与原始数据集具有相同的列。如果有人上传的电子表格不符合此要求,将无法解析。这个应该是大多数客户可以接受的限制。数据不符时,也可以尝试给客户一个提示信息。

    Excel 导入导出效果

    最终的项目可以参考下面的附件 https://gcdn.grapecity.com.cn/forum.php?mod=attachment&aid=MjUzNTE4fGU5MTk4OGQxfDE2NzM0MTYxMjd8NjI2NzZ8OTk3MTg%3D

    React 、Redux 和 电子表格的配合使用让这个应用的增强开发变的非常方便。借助 Redux 提供的可预测化的状态管理和交互式电子表格,可以在很短内创建复杂的企业 JavaScript 应用程序。

    拓展阅读

    React + Springboot + Quartz ,从 0 实现 Excel 报表自动化

    电子表格也能做购物车?简单三步就能实现

    使用纯前端类 Excel 表格控件 SpreadJS 构建企业现金流量表

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2770 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 15:37 · PVG 23:37 · LAX 08:37 · JFK 11:37
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.