期末复习

软开(二):React高级Component

2026 年 06 月 29 日 约 8274 字 · 21 分钟 期末复习

React 高级组件与项目架构学习报告

本报告基于教材 demo1101 等核心页面,讲解实际项目中的组件使用方式。


一、布局组件 Layout

教材使用 Ant Design 的 Layout 组件实现”左树右表”的经典布局。

1.1 基本结构

import { Layout } from 'antd';
const { Content, Sider } = Layout;

render() {
    return (
        <Layout style=>
            {/* 左侧:树 */}
            <Sider theme='light' width={250} style=>
                {/* 放树组件 */}
            </Sider>

            {/* 右侧:表格 */}
            <Content style=>
                {/* 放表格组件 */}
            </Content>
        </Layout>
    );
}

1.2 可拖动分隔线(Resizable)

import { Resizable } from 'react-resizable';

// state 中必须存宽度(变化时要重新渲染)
state = {
    splitSize: { width: 250 }
}

render() {
    let siderwidth = this.state.splitSize.width;

    return (
        <Layout>
            <Resizable
                width={siderwidth}
                minConstraints={[200, Infinity]}  // 最小宽度 200px
                maxConstraints={[500, Infinity]}  // 最大宽度 500px
                axis="x"                          // 只能横向拖动
                onResize={this.handleResize}       // 定义在 MyFormComponent 中
                handle={sys.resizeVHandle}         // 拖拽把手(来自 functions.js)
            >
                <Sider width={siderwidth}>
                    {/* 左边内容 */}
                </Sider>
            </Resizable>
            <Content>
                {/* 右边内容 */}
            </Content>
        </Layout>
    );
}

handleResize 定义在父类 MyFormComponent 里,自动更新 this.state.splitSize


二、ref 机制——父组件控制子组件

2.1 什么是 ref?

正常情况下,父组件只能通过 props 向子组件传数据(单向)。 ref 让父组件直接持有子组件的类实例,从而可以:

  • 读取子组件的 state
  • 调用子组件的内部方法

2.2 ref 的写法

// 在 JSX 中绑定 ref
<AntdTree ref={ref => this.myTree1 = ref} />
<AntdTable ref={ref => this.myTable1 = ref} />
  • ref => this.myTree1 = ref:这是一个回调函数。
  • 当组件挂载完成,React 自动执行这个函数,把组件实例传给 ref 参数。
  • this.myTree1 = ref 把实例存到父组件的成员变量上。
  • 不需要提前声明 this.myTree1,赋值时自动创建。

2.3 通过 ref 操作子组件

// 读取子组件的 state
let treeData = this.myTree1.state.data;
let currentPage = this.myTable1.state.pageno;

// 调用子组件的方法
this.myTable1.loadTableData();        // 重新加载表格数据
this.myTree1.focusTreeNode(node);     // 定位树节点

// 直接修改子组件的 state(谨慎使用)
this.myTable1.setState({ pageno: 1 });

三、props——父子组件通信

3.1 父传子(通过属性传递)

// 父组件:传递数据
render() {
    return (
        <MyChild
            name="张三"
            score={95}
            onSubmit={this.handleSubmit}  // 传递函数
        />
    );
}

// 子组件:通过 this.props 接收
export default class MyChild extends Component {
    render() {
        let { name, score, onSubmit } = this.props;
        return (
            <div>
                <p>{name} 的分数:{score}</p>
                <button onClick={onSubmit}>提交</button>
            </div>
        );
    }
}

3.2 子传父(通过回调函数)

// 父组件:定义回调,接收子组件传来的数据
handleSelectNode = (node) => {
    console.log('收到子组件传来的节点:', node);
    // 用 node 数据做事情...
}

render() {
    return (
        <AntdTree
            onSelectNode={(key, e) => this.handleSelectNode(e.node)}
        />
    );
}

// AntdTree 内部:用户点击时触发父组件的回调
// onSelectNode?.(key, e)  ← 把数据通过回调传给父组件

3.3 自定义属性传入自定义组件

AntdTreeAntdTable 等自定义组件通过 props 接收配置属性:

<AntdTree
    sqlprocedure="demo804a"   // 自定义属性:加载数据用的存储过程
    filterprocedure="demo804e" // 自定义属性:搜索用的存储过程
    loadstyle="expand"         // 自定义属性:加载方式
    root="全部商品分类"        // 自定义属性:根节点标题
/>

组件内部通过 parseParams(props) 解析,把所有属性都复制到 attr 对象里:

constructor(props) {
    let attr = parseParams(props);  // 解析所有传入属性
    // 之后通过 this.state.attr.sqlprocedure 访问
}

四、自定义封装组件详解

4.1 AntdTree 树组件

<AntdTree
    ref={ref => this.myTree1 = ref}           // 获取实例引用
    sqlprocedure="demo804a"                    // 加载数据的存储过程名
    filterprocedure="demo804e"                 // 搜索过滤的存储过程名
    loadstyle="expand"                         // 懒加载(expand)或全量加载(full)
    root="全部商品分类"                        // 虚拟根节点的显示文字
    filter="true"                              // 是否显示搜索框
    blockNode={true}                           // 节点撑满整行
    icon={<PaperClipOutlined />}               // 节点图标
    onSelectNode={(key, e) =>
        this.handleSelectNode(e.node)}         // 点击节点时的回调
/>

AntdTree 的自动行为:

  • 挂载时(componentDidMount)自动调用 sqlprocedure 加载数据
  • autoload=true(默认):自动加载
  • autofocus=true(默认):自动选中第一个节点

loadstyle 的区别: | 值 | 行为 | 适用场景 | |—-|——|———| | full | 一次性加载全部节点 | 数据量少的树 | | expand | 点击展开时才加载子节点 | 数据量大的树 |

4.2 AntdTable 表格组件

<AntdTable
    ref={ref => this.myTable1 = ref}
    columns={columns}                          // 列定义数组
    pagesize="20"                              // 每页显示行数
    keyfield='productid'                       // 主键字段名
    keytitle='商品'                            // 用于提示文字(如"删除商品记录")
    sqlprocedure='demo1101b'                   // 查询数据的存储过程
    categoryid=''                              // 传给存储过程的额外参数
    toolbar='-;add;-;edit;-;delete;-;refresh'  // 工具栏按钮
    rowbuttons='right'                         // 行内操作按钮显示在右侧
    rowselection='multiple'                    // 多选模式
    rownumber                                  // 显示序号列
    afterLoadData={this.afterLoadData.bind(this)} // 数据加载后的回调
    onAddRow={() => this.handleAddRow()}
    onEditRow={(row) => this.handleEditRow(row)}
    onDeleteRow={() => this.handleDeleteRow()}
    onSaveRow={() => this.handleSaveRow()}
    onDoubleClick={(row, index) => this.handleDoubleClick(row, index)}
/>

columns 列定义详解:

const columns = [
    {
        dataIndex: 'productid',    // 对应数据中的字段名
        title: '商品编码',          // 表头显示文字
        width: '80px',             // 列宽
        align: 'center',           // 对齐方式:center/left/right
        fixed: 'left',             // 固定列:left/right(不随滚动消失)
    },
    {
        dataIndex: 'unitprice',
        title: '单价',
        width: '80px',
        align: 'right',
        // 排序函数:a、b 是两行数据,返回负数/0/正数决定顺序
        sorter: (a, b) => a.unitprice - b.unitprice,
    },
    {
        dataIndex: 'productname',
        title: '商品名称',
        ellipsis: true,            // 文字超出宽度时显示 ... 省略号
    }
];

4.3 AntdInputBox 输入框组件

使用绝对定位,通过 topleft 精确控制位置:

const rowheight = 42;  // 每行间距

// ref={ref => this.fieldName = ref} 让父组件可以读取/控制输入框

<AntdInputBox
    type='text'
    id='productid'         // 对应表单字段名和 Form.Item 的 name
    label='商品编码'        // 左边的标签文字
    labelwidth='82'        // 标签宽度(px)
    left='2'               // 距左边距离
    width='140'            // 输入框宽度
    top={8 + rowheight * 0} // 第1行:8px + 0个行高
    ref={ref => this.productid = ref}
/>
<AntdInputBox
    type='text'
    id='productname'
    label='商品名称'
    labelwidth='82'
    left='2'
    width='300'
    top={8 + rowheight * 1}  // 第2行:8px + 1个行高
    ref={ref => this.productname = ref}
/>

type 可选值: | type | 说明 | |——|——| | text | 文本输入框 | | number | 数字输入框 | | date | 日期选择器 | | search | 带搜索按钮的输入框 |

4.4 AntdComboBox 下拉框组件

<AntdComboBox
    id='supplierid'          // 字段名
    label='供应商'
    labelwidth='82'
    left='2'
    width='300'
    top={8 + rowheight * 7}
    sqlprocedure='demo502d'  // 从数据库加载下拉选项
    textfield='suppliername' // 显示的文字字段
    valuefield='supplierid'  // 存储的值字段
    onChange={this.handleSupplierChange}
    ref={ref => this.supplierid = ref}
/>

4.5 AntdCascader 级联选择组件

用于选择树形结构的数据(如省市区、商品分类):

<AntdCascader
    page={this}             // 传入当前页面实例,组件内部需要访问表单
    form='myForm1'          // 关联的表单名
    id='subcategoryid'
    label='所属类别'
    labelwidth='82'
    left='2'
    width='300'
    top={8 + rowheight * 5}
    ref={ref => this.subcategoryid = ref}
    textfield='categoryname'  // 显示字段
    valuefield='subcategoryid' // 值字段
    sqlprocedure='demo505a'   // 加载树形选项的存储过程
    treestyle='full'
    onChange={this.handleCategoryChange.bind(this)}
/>

五、Modal 弹窗表单

教材使用 Modal + Form 的组合实现弹窗表单。

5.1 弹窗的开关控制

state = {
    myWin1: false,       // false=关闭,true=打开
    addoredit: 'update', // 当前操作类型
}

// 打开弹窗(新增模式)
handleAddRow = () => {
    this.setState({ myWin1: true, addoredit: 'add' }, () => {
        setTimeout(() => {
            this.resetFormValues('myForm1'); // 清空表单
        });
    });
}

// 打开弹窗(修改模式)
handleEditRow = (row) => {
    this.setState({ myWin1: true, addoredit: 'update', row }, () => {
        setTimeout(() => {
            this.setFormValues('myForm1', row);          // 填充数据
            this.setFormFields('myForm1', 'readOnly', false); // 可编辑
        });
    });
}

// 关闭弹窗
<Button onClick={() => this.setState({ myWin1: false })}>关闭</Button>

为什么要用 setTimeout? setState 是异步的,setState 执行完成、组件重渲染后,弹窗里的输入框组件才真正挂载到 DOM 上。setTimeout 让赋值操作等到下一个事件循环,确保输入框已经存在。

5.2 弹窗的完整 JSX 结构

<Modal
    name='myWin1'
    title='商品详细信息'
    open={this.state.myWin1}          // 控制显示/隐藏
    width={465}
    forceRender                        // 强制预渲染内容(ref 需要)
    centered                           // 居中显示
    maskClosable={false}               // 点击遮罩不关闭
    keyboard={false}                   // 按 Esc 不关闭
    footer={[
        <Button
            key='btnok'
            type='primary'
            disabled={this.state.addoredit === 'query'} // 查询模式禁用保存
            onClick={this.handleSaveRow}
        >
            保存
        </Button>,
        <Button key='btnclose' onClick={() => this.setState({ myWin1: false })}>
            关闭
        </Button>
    ]}
>
    <Form name="myForm1" ref={ref => this.myForm1 = ref}>
        <AntdInputBox id='productid' label='商品编码' ... />
        <AntdInputBox id='productname' label='商品名称' ... />
        {/* 更多字段... */}
    </Form>
</Modal>

六、CRUD 操作的标准流程

6.1 新增(Add)

handleAddRow = () => {
    // 1. 打开弹窗,设置为新增模式
    this.setState({ myWin1: true, addoredit: 'add' }, () => {
        setTimeout(() => {
            // 2. 清空表单所有字段
            this.resetFormValues('myForm1');
            // 3. 主键字段设为可编辑(新增时自己填)
            this[this.myTable1.state.keyfield].setState({ readOnly: false });
        });
    });
}

6.2 修改(Edit)

handleEditRow = (row) => {
    if (!row) row = this.myTable1.state.row;  // 没传就取当前选中行
    this.setState({ myWin1: true, addoredit: 'update', row }, () => {
        setTimeout(() => {
            // 填充表单(同时返回填充前的旧数据)
            let lastrow = this.setFormValues('myForm1', row);
            // 设置所有字段可编辑
            this.setFormFields('myForm1', 'readOnly', false);
            // 主键字段设为只读(修改时不能改主键)
            this[this.myTable1.state.keyfield].setState({ readOnly: true });
        });
    });
}

6.3 删除(Delete)

handleDeleteRow = async () => {
    // deleteTableRow 是 MyFormComponent 提供的方法
    // 它会弹出确认框,确认后调用存储过程删除
    return (await this.deleteTableRow(this.myTable1, 'myForm1', 'demo901a'));
}

6.4 保存(Save)

handleSaveRow = async () => {
    // saveTableRow 是 MyFormComponent 提供的方法
    // 它读取表单数据,调用存储过程保存,返回结果
    let rs = await this.saveTableRow(this.myTable1, 'myForm1', 'demo1101c');

    if (rs && rs.error === '' && rs.rows.length > 0) {
        // 保存成功后的逻辑:定位到保存的记录
        let row = rs.rows[0];
        // ... 计算页码、定位树节点等
    }

    // 关闭弹窗
    this.setState({ addoredit: 'update', myWin1: false });
}

七、树与表格联动

这是教材最核心的交互模式:点击左侧树节点 → 右侧表格按分类筛选刷新

handleSelectNode = async (node) => {
    // node 是被点击的树节点对象
    // node.subcategoryid 是该节点的分类 ID

    // 更新表格的筛选参数,重置到第一页,然后重新加载
    this.myTable1?.setState({
        pageno: 1,
        attr: {
            ...this.myTable1.state.attr,
            // treefield = 'subcategoryid',表格存储过程用这个参数筛选
            categoryid: node[this.state.myTable1.treefield] || ''
        }
    }, () => {
        setTimeout(() => {
            this.myTable1?.loadTableData();  // 重新请求数据库
        });
    });
}

绑定到树组件:

<AntdTree
    onSelectNode={(key, e) => this.handleSelectNode(e.node)}
    // e.node 是被点击的节点对象,包含所有字段
/>