基于AntD-Table的可编辑输入表格

文章目录

  • 需求描述
    • 基本要实现的共嗯那个
    • 不能实现的功能
  • 设计思路
    • 结构方案基于适配器模式
    • AntD的table定制
  • 实现结构
    • Adapter操作类
      • 职责功能
      • 技术重点
        • adapter 操作类相关部分
    • 单元格菜单
      • 职责功能
      • 技术重点
      • 实例代码
    • 可编辑表格组件
      • 功能职责
      • 技术重点
      • 实例代码

需求描述

基本要实现的共嗯那个

  1. 基于AntD的table控件,实现类似PPT中【表格】的功能
  2. 可以基于配置的行数(nRow)、列数(nColumn)信息生成指定尺寸的表格,且每一个单元格都是一个可入输入框
  3. 每一个输入框在输入完成后自动将输入值记录到组件state或者外部state中,而不是类似表单的【提交前统一获取值】
  4. 可以动态地在表格的任意位置追加、插入、删除一行或一列
  5. 可以在不改变内部数据的情况下,动态地实现指定列、行的隐藏与显示
  6. 可以实现表格的行列转置
  7. 可以动态的设定每一个单元格的文本样式(颜色、字体、粗细、对其方式)以及单元格样式(边框颜色、宽度、背景颜色)
  8. 可以将当前单元格的样式应用到所在行或着所在列
  9. 可以以非拖动单元格边框的的方式对指定的列宽进行动态修改

不能实现的功能

  1. 类似excel中拖动单元格边框调整单元格所在行、列的高度和宽度
  2. 同时选中任意多个单元格,统一设置单元格样式

设计思路

结构方案基于适配器模式

  1. 数据data:只有要展示或记录的文本信息
  2. Adapter:
    1. 负责将data中文本内容生成Table组件可以识别的datasource对象
    2. 负责维护,操作,table和cell的样式与提供必要的功能
  3. Table:
    1. 负责展示datasource中的数据
    2. 负责完成UI层面的交互与时间监听,并调用adapter的接口实现对应功能

AntD的table定制

  1. 覆盖原有的table的Components属性,指定datasouce在渲染行、列时采用自定义的行、列组件
  2. 自定义行组件:外套一个form组件,便于对内部的cell组件进行统一的赋值和取值
  3. 自定义单元格组件:
    1. 返回一个from.Item-Input组件,便于设置初始值和监听输入值变换
    2. 上述组件外包一个Popover,套接一个操作菜单,方便对指定单元格完成各种操作

简单来说:一个表格控件的的每一行都是一个表单,一行里每个单元个都是这个表单的输入项目,每个输入项目都有一个弹出菜单

实现结构

完整代码参考 gitee

Adapter操作类

职责功能

  1. 完成数据源json文件的解析,并生成AntD-Table可以识别的dataSource
  2. 提供对表结构的存储、维护与修改API
  3. 完成对单元格样式、单元格文本样式的修改API

技术重点

  1. 所有从datajson中读取到的数据,都是以一维数组的形式存储在adapter的datas属性中的,只有在在调用toDataSource()方法后,adapter才会按照指定的nRownColumn生成antd-table控件可识别的datasource对象

  2. toColumns(onCellHandle)参数,会将antd-table在绘制每个单元格时的默认参数向外传递,可以通过扩展onCellHandle的返回值,实现单元格绘制的传参和事件监听

adapter 操作类相关部分
import * as _ from "lodash"

/**
 * 可编辑表格内部的单元格实体类
 */
class EdittableCellData {
    constructor(text = "", style = undefined, editable = false) {
        this.text = text
        //定义单元格的缺省样式(即在配置文件中没有指定时的样式)
        if (style === undefined)
            this.style = {
                fontWeight: 500,
                fontSize: "14px",
                fontStyle: "normal",
                textAlign: "center",
                backgroundColor: "transparent",
                color: 'gray',
                fontFamily: '微软雅黑',
                columnWidth: undefined
            }
        else {
            this.style = style
        }
        this.editable = editable
    }
}

/**
 * 可编辑表格的Adapter实体类
 */
class EdittableAdapter {
    constructor() {
        /**
         * 单元格数据与样式数据
         */
        this.datas = undefined;
        /**
         * 当前表格的总行数
         */
        this.nRow = undefined;
        /**
         * 当前表格的总列数
         */
        this.nColumn = undefined;
        /**
         * 当前表格的总体样式
         */
        this.style = undefined;
        /**
         * table的key
         */
        this.prefix = undefined;
        /**
         * 需要跳过不显示的列索引数组
         */
        this.skipColums = []
    }

    /**
     * 指定行数和列数,构造具有默认样式的适配器对象
     * @param {number} initialRows 
     * @param {number} initialCols 
     * @returns 修改后的适配器对象本身
     */
    initial(initialRows, initialCols) {
        this.datas = new Array(initialRows * initialCols)
        this.nRow = initialRows;
        this.nColumn = initialCols;
        this.style = undefined;
        this.datas = _.fill(this.datas, new EdittableCellData())
        return this;
    }

    /**
     * 从配置文件中读取配置参数,并实例化当前适配器对象
     * @param {object} dataConfig 
     * @returns 修改后的适配器对象本身
     */
    loadConfig(dataConfig) {
        this.nRow = dataConfig.nRow
        this.nColumn = dataConfig.nColumn
        this.style = dataConfig.style
        this.datas = dataConfig.datas
        this.datas.forEach(value => {
            //如果当前单元格没有默认样式,则赋值为表格统一的样式
            Object.keys(this.style).forEach(propsName => {
                if (value.style.hasOwnProperty(propsName) === false) {
                    value.style[propsName] = this.style[propsName]
                }
            })
        })
        this.prefix = dataConfig.prefix;
        this.skipColumns = [...dataConfig.skipColumns];
        return this;
    }

    toJsonConfig() {
        return JSON.stringify(this)
    }

    /**
    * 深拷贝方式构建一个新的适配器对象
    * @returns 新的适配器对象
    */
    toNewInstance() {
        return new EdittableAdapter().loadConfig(JSON.parse(this.toJsonConfig()))
    }

    /**
     * 生成一行数据对应的json对象的key值,主要用于table控件的columns进行统一
     * @param {number} col 从0开始的列序号
     * @returns [prefix+"_"]+"col-"+col
     */
    _itemColKey(col) {
        if (this.prefix)
            return this.prefix + "_col-" + col
        else
            return "col-" + col
    }

    /**
     * 将ColKey转换为数字形式的列序号
     * @param  {string}  形如\[prefix+"_"]+"col-"+col
     * @returns 从0开始的列序号
     */
    _itemColKeyIndex(colkey){
        return colkey.split("-")[1] * 1
    }

    /**
     * 按照行列数信息转换为AntD的Table组件可以识别的数据源
     * @returns 元素形如{key:0,col-0:a,col-1:b}数组,一个元素为Table组件中的一行
     */
     toDataSource() {
        let result = []
        for (let row = 0; row < this.nRow; row++) {
            let tempRow = { "key": row }
            for (let col = 0; col < this.nColumn; col++) {
                tempRow[this._itemColKey(col)] = this.datas[row * this.nColumn + col]
            }
            result.push(tempRow)
        }
        return result;
    }

    /**
     * 生成为AntD的Table组件可以识别的列名数组
     * @param {*} onCellHandle 绘制每个单元格的回调,参数为{行号,列名,单元格内容}
     * @returns 
     */
     toColumns(onCellHandle) {
        let result = []
        for (let col = 0; col < this.nColumn; col++) {
            //跳过忽略列
            if (this.skipColumns.includes(col) === false) {
                result.push({
                    "title": this._itemColKey(col),
                    "dataIndex": this._itemColKey(col),
                    "onCell": (record, rowIndex) => {
                        if (onCellHandle)
                            return onCellHandle(rowIndex, this._itemColKey(col), record[this._itemColKey(col)])
                    },
                })
            }
        }
        return result
    }

     /**
     * 内容行列互换,改方法修改当前数据本身
     */
      transpose() {
        let result = new Array(this.nColumn * this.nRow)
        for (let row = 0; row < this.nRow; row++) {
            for (let col = 0; col < this.nColumn; col++) {
                let { text, style, editable } = this.datas[row * this.nColumn + col]
                result[col * this.nRow + row] = new EdittableCellData(text, style, editable)
            }
        }
        this.datas = Object.assign(result)
        let temp = this.nRow;
        this.nRow = this.nColumn;
        this.nColumn = temp
        return this;
    }

    /**
     * 利用新数组对元表格的中值进行替换,该方法不修改单元格的样式
     * @param {array} newDataArray 
     * @returns 修改后的适配器本身
     */
     fillCellData(newDataArray) {
        //只有新值数组和原始数组长度一直时才进行替换,如果
        if (newDataArray.length === this.datas.length) {
            let newDatas = []
            for (let index = 0; index < this.datas.length; index++) {
                let { style, editable } = this.datas[index]
                newDatas.push(new EdittableCellData(newDataArray[index], style, editable))
            }
            this.datas = [...newDatas]
        }
        return this;
    }

    /**
     * 填充或修改表格中一行的值
     * @param {number} rowIndex 行序号:从0开始
     * @param { \{key:0,col-0:a,col-1:b}} rowData 用于填充或修改的对象:形如{key:0,col-0:a,col-1:b}
     * @returns 修改后的适配器本身
     */
     fillRowData(rowIndex, rowData) {
        for (let row = 0; row < this.nRow; row++) {
            if (rowIndex === row) {
                for (let col = 0; col < this.nColumn; col++) {
                    let { style, editable } = this.datas[rowIndex * this.nColumn + col]
                    this.datas[rowIndex * this.nColumn + col] = new EdittableCellData(rowData[this._itemColKey(col)], style, editable)
                }
            }
        }
        return this;
    }

    /***
     * 获取指定行序号和列标签的单元格的样式
     *  @param {number} rowIndex 行序号:从0开始
     *  @param {string} colkey 列标签,形如\[prefix+"_"]+"col-"+col
     *  @returns 指定单元格的样式,以css实体类形式存储
     */
    getCellStyle(rowIndex, colkey) {
        let colIndex = this._itemColKeyIndex(colkey)
        return this.datas[rowIndex * this.nColumn + colIndex].style
    }

    /***
     * 更新指定行序号和列标签的单元格的样式
     *  @param {number} rowIndex 行序号:从0开始
     *  @param {string} colkey 列标签,形如\[prefix+"_"]+"col-"+col
     *  @param {string} cssStyle css实体类形式存储的样式
     *  @returns 修改后的适配器本身
     */
    updateCellStyle(rowIndex, colkey, cssStyle) {
        let colIndex = this._itemColKeyIndex(colkey)
        if (cssStyle !== undefined) {
            let { text, editable } = this.datas[rowIndex * this.nColumn + colIndex]
            this.datas[rowIndex * this.nColumn + colIndex] = new EdittableCellData(text, cssStyle, editable)
        }
        return this;
    }

     /***
     * 更新所有的单元格的样式
     *  @param {string} cssStyle css实体类形式存储的样式
     *  @returns 修改后的适配器本身
     */
    updateAllCellsStyle(cssStyle) {
        this.style = Object.assign(cssStyle)
        for (let row = 0; row < this.nRow; row++) {
            for (let col = 0; col < this.nColumn; col++) {
                let { text, editable } = this.datas[row * this.nColumn + col]
                this.datas[row * this.nColumn + col] = new EdittableCellData(text, cssStyle, editable)
            }
        }
        return this;
    }
    /***
     * 更新指定行的单元格的样式
     *  @param {number} rowIndex 行序号:从0开始
     *  @param {string} cssStyle css实体类形式存储的样式
     *  @returns 修改后的适配器本身
     */
    updateRowStyle(rowindex, cssStyle) {
        for (let col = 0; col < this.nColumn; col++) {
            this.updateCellStyle(rowindex, this._itemColKey(col), cssStyle)
        }
        return this;
    }

     /***
     * 更新指定行的单元格的样式
     *  @param {string} colkey 列标签,形如\[prefix+"_"]+"col-"+col
     *  @param {string} cssStyle css实体类形式存储的样式
     *  @returns 修改后的适配器本身
     */
    updateColumnStyle(colindex, cssStyle) {
        for (let row = 0; row < this.nRow; row++) {
            this.updateCellStyle(row, colindex, cssStyle)
        }
        return this;
    }

    /**
     * 强制调整指定列内所有单元格的宽度
     * @param {string} colkey 列标签,形如\[prefix+"_"]+"col-"+col
     * @param {number} columnWidth 列宽的像素数值
     * @returns 
     */
    updateColumnWidth(colkey, columnWidth) {
        for (let row = 0; row < this.nRow; row++) {
            let style = this.getCellStyle(row, colkey)
            let newStyle = { ...style, columnWidth: columnWidth + "px" }
            this.updateCellStyle(row, colkey, newStyle)
        }
        return this;
    }

    /**
     * 从startRow行开始,在其上一行位置插入一行元素
    */
     insertRow(startRow) {
        let head = _.slice(this.datas, 0, startRow * this.nColumn)
        let temp = _.fill(new Array(this.nColumn), new EdittableCellData("", this.style, true))
        let tail = _.slice(this.datas, startRow * this.nColumn)
        this.datas = Object.assign([...head, ...temp, ...tail])
        this.nRow = this.datas.length / this.nColumn
    }


    /**
     * 从startRow行开始,在其下一行的位置追加一行元素
     * */
    appendRow(startRow) {
        let head = _.slice(this.datas, 0, (startRow + 1) * this.nColumn)
        let temp = _.fill(new Array(this.nColumn), new EdittableCellData("", this.style, true))
        let tail = _.slice(this.datas, (startRow + 1) * this.nColumn)
        this.datas = Object.assign([...head, ...temp, ...tail])
        this.nRow = this.datas.length / this.nColumn
    }

    /**
     * 删除指定startRow索引行的元素
     */
    deleteRow(startRow) {
        if (startRow + 1 === this.nRow) {
            this.datas = Object.assign(_.slice(this.datas, 0, (startRow) * this.nColumn))
        }
        else {
            let head = _.slice(this.datas, 0, (startRow) * this.nColumn)
            let tail = _.slice(this.datas, (startRow + 1) * this.nColumn)
            this.datas = Object.assign([...head, ...tail])
        }
        this.nRow = this.datas.length / this.nColumn
    }

    insertColumn(startCol) {
        startCol = startCol * 1
        let result = []
        for (let row = 0; row < this.nRow; row++)
            for (let col = 0; col < this.nColumn; col++) {
                if (startCol === col) {
                    result.push(new EdittableCellData("", this.style, true))
                }
                result.push(this.datas[row * this.nColumn + col])
            }
        this.datas = Object.assign(result)
        this.nColumn = this.datas.length / this.nRow
    }

    appendColumn(startCol) {
        startCol = startCol * 1
        let result = []
        if (startCol + 1 >= this.nColumn) {
            for (let row = 0; row < this.nRow; row++) {
                for (let col = 0; col < this.nColumn; col++) {
                    result.push(this.datas[row * this.nColumn + col])
                }
                result.push(new EdittableCellData("", this.style, true))
            }
        }
        else {
            for (let row = 0; row < this.nRow; row++)
                for (let col = 0; col < this.nColumn; col++) {
                    if (startCol + 1 === col) {
                        result.push(new EdittableCellData("", this.style, true))
                    }
                    result.push(this.datas[row * this.nColumn + col])
                }
        }
        this.datas = Object.assign(result)
        this.nColumn = this.datas.length / this.nRow
    }

    deleteColumn(startCol) {
        startCol = startCol * 1
        let result = []
        for (let row = 0; row < this.nRow; row++) {
            for (let col = 0; col < this.nColumn; col++) {
                if (startCol !== col) {
                    result.push(this.datas[row * this.nColumn + col])
                }
            }
        }
        this.datas = Object.assign(result)
        this.nColumn = this.datas.length / this.nRow
    }


}

export default EdittableAdapter

单元格菜单

职责功能

  1. 定义单元格菜单的UI与绑定与之交互的事件
  2. 对菜单的UI的各类事件,进行分组包装,向外部提供统一规范的回调接口

技术重点

  1. 单元格的任何样式变换,都会通过props.onStyleChanged回调将相关参数传递给调用者
  2. 表格的任何结构变换,都会通过props.onStructChanged回调将相关参数传递给调用者
  3. 菜单中各UI组件的自身回调,都会将响应参数统一化处理后,调用上述两个props的回调

实例代码

import { Select, Radio, Button, Dropdown, Menu, Tooltip } from 'antd';
import {
    BoldOutlined,
    ItalicOutlined,
    AlignLeftOutlined,
    AlignCenterOutlined,
    AlignRightOutlined,
    PlusSquareOutlined,
    MinusSquareOutlined,
    VerticalLeftOutlined,
    VerticalRightOutlined
} from '@ant-design/icons';
import React, { useEffect, useState } from 'react';
import "./EdittableCellMenu.scss"
import ColorPicker from './colorPicker/ColorPicker';
import config from "./EdittableCellMenuConfig.json"

/**
 * 单元格弹出菜单组件,基于react-Hook编写
 * @param {*} props 
 * @returns 
 */
 function EdittableCellContextMenu(props) {

    const [cssStyle, SetCssStyle] = useState(props.cssStyle)
    const [deleteRowEnable, setDeleteRowEnable] = useState(false)
    const [deleteColumnEnable, setDeleteColumnEnable] = useState(false)

    useEffect(() => {
        setDeleteRowEnable(props.nrow <= 1)
    }, [props.nrow])

    useEffect(() => {
        setDeleteColumnEnable(props.ncolumn <= 1)
    }, [props.ncolumn])

    function generateFontsizeOption() {
        return config.fontSizeList.map(value => {
            return <Select.Option key={value} value={value + "px"}>{value}</Select.Option>
        })
    }

    function generateFontOption() {
        return config.fonts.map(value => <Select.Option className="fontname-option"
            key={value.name}
            style={{ fontSize: "5px" }}
            value={value.name}>{`${value.alias}`}
        </Select.Option>)
    }

    function genearateAddMenu() {
        return <Menu onClick={onAddMenuItemClicked}>
            <Menu.Item key="insertRow">上方插入一行</Menu.Item>
            <Menu.Item key="appendRow">下方追加一行</Menu.Item>
            <Menu.Item key="insertColumn">左侧插入一列</Menu.Item>
            <Menu.Item key="appendColumn">右侧追加一列</Menu.Item>
        </Menu>
    }

    function genearateRemoveMenu() {
        return <Menu onClick={onDeleteItemClicked}>
            <Menu.Item key="deleteRow" disabled={deleteRowEnable}>删除当前行</Menu.Item>
            <Menu.Item key="deleteColumn" disabled={deleteColumnEnable}>删除当前列</Menu.Item>
        </Menu>
    }

    function genearateStyleApplyMenu() {
        return <Menu onClick={(item) => onStyleApplyedClicked(item.key)}>
            <Menu.Item key="row" disabled={deleteRowEnable}>应用到行</Menu.Item>
            <Menu.Item key="column" disabled={deleteColumnEnable}>应用到列</Menu.Item>
            <Menu.Item key="reset">清除样式</Menu.Item>
        </Menu>
    }


    function executeStructCallback(key) {
        if (props.onStructChanged) {
            props.onStructChanged(key, props.rowindex, props.colindex)
        }
    }

    function executeStyleCallback(cssStyle) {
        if (props.onStyleChanged) {
            props.onStyleChanged(cssStyle, props.rowindex, props.colindex)
            SetCssStyle(cssStyle)
        }
    }


    function onAlignTypeChanged(e) {
        executeStyleCallback({ ...cssStyle, textAlign: e.target.value })

    }

    function onFontSizeChanged(value) {
        executeStyleCallback({ ...cssStyle, fontSize: value })
    }

    function onFontChanged(value) {
        executeStyleCallback({ ...cssStyle, fontFamily: value })
    }

    function onColorPickerChanged(type, color) {
        if (type === "fill") {
            executeStyleCallback({ ...cssStyle, backgroundColor: color })
        }
        else {
            executeStyleCallback({ ...cssStyle, color: color })
        }
    }

    function onFontBoldChanged(e) {
        let { fontWeight } = cssStyle
        let temp = fontWeight === 700 ? 500 : 700;
        executeStyleCallback({ ...cssStyle, fontWeight: temp })
    }

    function onFontItalicChanged(e) {
        let { fontStyle } = cssStyle
        let temp = fontStyle === "normal" ? "italic" : "normal";
        executeStyleCallback({ ...cssStyle, fontStyle: temp })
    }

    function onAddMenuItemClicked(item) {
        executeStructCallback(item.key)
    }

    function onDeleteItemClicked(item) {
        executeStructCallback(item.key)
    }

    function onStyleApplyedClicked(type) {
        if (props.onStyleApplyed) {
            props.onStyleApplyed(type, cssStyle, props.rowindex, props.colindex)
        }
    }

    function onColumnWidthClicked(type) {
        if (props.onColumnWidthClicked) {
            props.onColumnWidthClicked(type, props.rowindex, props.colindex)
        }
    }


    return (
        <div className="edittable-toobar-warpper">
            <Select
                bordered={false}
                defaultValue={cssStyle.fontFamily}
                size="small"
                onSelect={onFontChanged}>
                {generateFontOption()}
            </Select>
            <span className={"edittable-toobar-verticalline"}>|</span>
            <Select
                bordered={false}
                defaultValue={cssStyle.fontSize}
                size="small"
                onSelect={onFontSizeChanged}>
                {generateFontsizeOption()}
            </Select>
            <span className={"edittable-toobar-verticalline"}>|</span>
            <Button
                icon={<BoldOutlined />}
                className={cssStyle.fontWeight === 700 ? "active" : ""}
                size="small"
                type="text"
                onClick={onFontBoldChanged}>
            </Button>
            <span className={"edittable-toobar-verticalline"}>|</span>
            <Button
                icon={<ItalicOutlined />}
                className={cssStyle.fontStyle === "italic" ? "active" : ""}
                size="small"
                type="text"
                onClick={onFontItalicChanged}>
            </Button>
            <span className={"edittable-toobar-verticalline"}>|</span>
            <ColorPicker
                color={cssStyle.backgroundColor === "transparent" ? "gray" : cssStyle.backgroundColor}
                type="fill"
                onChange={onColorPickerChanged}></ColorPicker>
            <span className={"edittable-toobar-verticalline"}>|</span>
            <ColorPicker
                color={cssStyle.color}
                type="text"
                onChange={onColorPickerChanged}></ColorPicker>
            <span className={"edittable-toobar-verticalline"}>|</span>
            <Radio.Group
                size="small"
                defaultValue={cssStyle.textAlign}
                onChange={onAlignTypeChanged}>
                <Radio.Button value="left"><AlignLeftOutlined /></Radio.Button>
                <Radio.Button value="center"><AlignCenterOutlined /></Radio.Button>
                <Radio.Button value="right"><AlignRightOutlined /></Radio.Button>
            </Radio.Group>
            <span className={"edittable-toobar-verticalline"}>|</span>
            <Radio.Group
                size="small">
                <Tooltip placement="topLeft" title="加宽列">
                    <Button
                        type="text"
                        onClick={() => onColumnWidthClicked("add")}
                        icon={<VerticalLeftOutlined />}></Button>
                </Tooltip>
                <Tooltip placement="topLeft" title="收缩列">
                    <Button
                        type="text"
                        onClick={() => onColumnWidthClicked("reduce")}
                        icon={<VerticalRightOutlined />}></Button>
                </Tooltip>
            </Radio.Group>
            <span className={"edittable-toobar-verticalline"}>|</span>
            <Dropdown
                placement="topCenter"
                overlay={genearateStyleApplyMenu()}>
                <Button
                    size="small"
                    type="text">样式应用</Button>
            </Dropdown>
            <span className={"edittable-toobar-verticalline"}>|</span>
            <Dropdown
                placement="topCenter"
                overlay={genearateAddMenu()}>
                <Button
                    size="small"
                    type="text"
                    icon={<PlusSquareOutlined />}></Button>
            </Dropdown>
            <Dropdown
                placement="topCenter"
                overlay={genearateRemoveMenu()}>
                <Button
                    size="small"
                    type="text"
                    icon={<MinusSquareOutlined />}></Button>
            </Dropdown>
        </div>
    );
}

EdittableCellContextMenu.appendRow = "appendRow"
EdittableCellContextMenu.insertRow = "insertRow"
EdittableCellContextMenu.deleteRow = "deleteRow"
EdittableCellContextMenu.insertColumn = "insertColumn"
EdittableCellContextMenu.appendColumn = "appendColumn"
EdittableCellContextMenu.deleteColumn = "deleteColumn"

export default EdittableCellContextMenu;

可编辑表格组件

功能职责

  1. 实现自定义的【可编辑行】和【可编辑单元格】组件,并覆盖antd-table的components属性,组装在一起
  2. 协调table-row-cell的参数传递与事件绑定
  3. 通过调用adapter实现完成表格的结构变换、内容变换和样式变换

技术重点

  1. _onCellHandle函数的参数有antd自己提供,返回值将作为EdittableCellComponent组件的props参数传入

实例代码


import React, { Component, useContext, useEffect } from 'react';
import { Form, Table, Popover, Input } from "antd"
import EdittableAdapter from './EdittableAdapter';
import "./EdittableComponent.scss"
import EdittableCellContextMenu from './EdittableCellMenu';

const EditableContext = React.createContext(null);

class EdittableComponent extends Component {

    constructor(props) {
        super(props)
        this.state = {
            adapter: new EdittableAdapter().loadConfig(this.props.dataSource),
            width: props.width ? props.width : 2000,
            height: props.height ? props.height : 200,
        }
        this.onCellDataChanged = props.onCellDataChanged
    }

    /**
     * 自定义绘制单元格的回调,当前函数的返回值,会作为EdittableCellComponent的props参数被传入
     * @param {*} rowindex 
     * @param {*} colindex 
     * @param {*} cell 
     * @returns 
     */
    _onCellHandle = (rowindex, colindex, cell) => {
        let { adapter } = this.state;
        return {
            rowindex,
            colindex,
            cell,
            width: this.state.width,
            height: this.state.height,
            nRow: adapter.nRow,
            nColumn: adapter.nColumn,
            //单元格输入框值变化时,发生的回调,参数包括:当前触发行,当前触发行的所有值,当前触发列,当前修改值
            onCellDataChanged: (rowindex, values, colindex, value) => {
                let { adapter } = this.state;
                this.setState({ adapter: adapter.fillRowData(rowindex, values).toNewInstance() }, () => {
                    //向外传递数据修改回调
                    if (this.onCellDataChanged) {
                        this.onCellDataChanged(rowindex, colindex, value, adapter.toDataSource())
                    }
                })

            },
            onCellStyleChanged: (cssStyle, rowindex, colindex) => {
                let { adapter } = this.state;
                this.setState({ adapter: adapter.updateCellStyle(rowindex, colindex, cssStyle).toNewInstance() })
            },
            onTableStructChanged: (key, rowindex, colindex) => {
                let { adapter } = this.state;
                switch (key) {
                    case EdittableCellContextMenu.appendRow:
                        adapter.appendRow(rowindex)
                        break;
                    case EdittableCellContextMenu.insertRow:
                        adapter.insertRow(rowindex)
                        break;
                    case EdittableCellContextMenu.insertColumn:
                        adapter.insertColumn(adapter._itemColKeyIndex(colindex))
                        break;
                    case EdittableCellContextMenu.appendColumn:
                        adapter.appendColumn(adapter._itemColKeyIndex(colindex))
                        break;
                    case EdittableCellContextMenu.deleteColumn:
                        adapter.deleteColumn(adapter._itemColKeyIndex(colindex))
                        break;
                    default:
                        adapter.deleteRow(rowindex)
                        break;
                }
                this.setState({ adapter: adapter.toNewInstance() })
            },
            onStyleApplyed: (type, cssStyle, rowindex, colindex) => {
                let { adapter } = this.state;
                if (type === "row") {
                    this.setState({ adapter: adapter.updateRowStyle(rowindex, cssStyle).toNewInstance() })
                }
                else if (type === "column") {
                    this.setState({ adapter: adapter.updateColumnStyle(colindex, cssStyle).toNewInstance() })
                }
                else {
                    let tableStyle = adapter.style
                    this.setState({ adapter: adapter.updateAllCellsStyle(tableStyle).toNewInstance() })
                }
            },
            onColumnWidthClicked: (type, rowindex, colindex) => {
                let { adapter } = this.state;
                let initalWidth = this.state.width / adapter.nColumn
                let { columnWidth } = adapter.getCellStyle(rowindex, colindex)
                let newWidth = 0;
                if (columnWidth === undefined || columnWidth === null) {
                    newWidth = type === "add" ? initalWidth + 5 : initalWidth - 5
                }
                else {
                    columnWidth = columnWidth.split("p")[0] * 1
                    newWidth = type === "add" ? columnWidth + 5 : columnWidth - 5
                }
                this.setState({ adapter: adapter.updateColumnWidth(colindex, newWidth).toNewInstance() })
            }
        }
    }

    render() {
        //注意此处覆盖了默认table的components内容,改用自定义组件完成输入功能
        let { adapter, width, height } = this.state;
        return (
            <div className="edittable-warpper" style={{ width: width + "px", height: height + "px" }}>
                <Table
                    showHeader={false}
                    pagination={false}
                    columns={adapter ? adapter.toColumns(this._onCellHandle) : []}
                    dataSource={adapter ? adapter.toDataSource() : undefined}
                    components={{
                        body: {
                            row: EdittableRowComponent,
                            cell: EdittableCellComponent,
                        }
                    }}>
                </Table>
            </div>
        );
    }
}

/**
 * 自定义行组件:主要用于在行元素中包裹一个Form组件,便于单元格的赋值与取值
 * @param {*} props 
 * @returns 
 */
const EdittableRowComponent = (props) => {
    const [form] = Form.useForm();
    //这里使用了EditableContext对象,实现组件间的form参数传递
    return (
        <Form
            form={form}
            component={false}>
            <EditableContext.Provider value={{ form }}>
                <tr {...props} />
            </EditableContext.Provider>
        </Form>
    );
}

/**
 * 自定义单元格组件
 * @param {*} 从table的 _onCellHandle函数返回的参数集合 + antd默认的其他props参数 
 * @returns 
 */
const EdittableCellComponent = ({
    onCellDataChanged,
    onCellStyleChanged,
    onTableStructChanged,
    onStyleApplyed,
    onColumnWidthClicked,
    colindex,
    rowindex,
    cell,
    editMode,
    height,
    nRow,
    nColumn,
    width,
    ...props }) => {

    //从父容器中获得form对象,方便设置值   
    const { form } = useContext(EditableContext);

    //如果dataSource中有值,则作为默认值显示在input中
    useEffect(() => {
        if (cell)
            form.setFieldsValue({ [colindex]: cell.text })
    }, [cell, colindex, form])

    //按照设置内容定制每个单元格的样式
    function generateStyle(height, nRow, cell) {
        let defaultSytle = {
            padding: "0px",
            height: (height / nRow) + "px"
        }
        if (cell && cell.style) {
            return { ...defaultSytle, ...cell.style }
        }
        else {
            return defaultSytle
        }
    }
    //按照设置内容定制指定列的款段
    function process_td_DomStyle(props) {
        if (cell && cell.style.columnWidth) {
            props.style = { width: cell.style.columnWidth }
        }
        return props
    }
    /**
     * 依据配置配置文件的data的-editMode单独指定当前单元格的可编辑性
     * @param {string} editMode 
     * @param {object} cell 
     * @returns 
     */
    function detectDisenable(editMode, cell) {
        if (editMode && editMode === "custom") {
            if (cell && cell.editable && cell.editable === true) {
                return false
            }
            return true
        }
        else {
            return false
        }
    }

    //实时记录表单输入的变化值
    async function save(rowindex, colindex) {
        try {
            const values = await form.validateFields();
            onCellDataChanged(rowindex, values, colindex, values[colindex])
        } catch (errInfo) {
            console.error('Save failed:', errInfo);
        }
    };

    /**
     * 每一个单元格都包裹一个单元格菜单,每一个单元都是一个Form.Item维护的Input
     */
    return <Popover
        overlayClassName="edittable-toobar-warpper"
        trigger="contextMenu" //右键弹出
        content={<EdittableCellContextMenu
            cssStyle={cell ? cell.style : {}}
            onStyleChanged={onCellStyleChanged}
            onStructChanged={onTableStructChanged}
            onStyleApplyed={onStyleApplyed}
            onColumnWidthClicked={onColumnWidthClicked}
            rowindex={rowindex}
            colindex={colindex}
            nrow={nRow}
            ncolumn={nColumn}
        ></EdittableCellContextMenu>}>
        <td {...process_td_DomStyle(props)}>
            <Form.Item
                name={colindex}>
                <Input
                    placeholder="请输出"
                    disabled={detectDisenable(editMode, cell)}
                    bordered={false} //移除input自身border样式,改为style配置修改
                    id={"" + rowindex + "-" + colindex}
                    style={generateStyle(height, nRow, cell)}
                    onPressEnter={() => save(rowindex, colindex)}
                    onBlur={() => save(rowindex, colindex)}
                />
            </Form.Item></td>
    </Popover>;

}

export default EdittableComponent;

你可能感兴趣的:(前端,react,react,表格输入控件)