期末复习

软开(六):例题(二)

2026 年 07 月 01 日 约 9148 字 · 23 分钟 期末复习

期末复习资料(二):React 综合应用


一、React 类组件生命周期(必考)

1.1 生命周期三个阶段

挂载阶段(只执行一次):
  constructor() → render() → componentDidMount()

更新阶段(state/props变化时):
  render() → componentDidUpdate()

卸载阶段:
  componentWillUnmount()

1.2 教材标准写法解析

export default class MyPage extends MyFormComponent {

    // ① state 定义
    state = {
        myTable1: {
            rowindex: 0,
            keyfield: 'productid',
            treefield: 'subcategoryid',
            lastrow: {},
        },
        myWin1: false,      // 弹窗开关
        addoredit: 'update' // 当前操作模式
    }

    // ② componentDidMount:组件挂载完毕后执行(最常见的初始化时机)
    componentDidMount = async () => {
        // 通常在这里执行:初始化、自动聚焦、默认选中第一行等
        let node = {};
        node.key = '_root';
        node[this.state.myTable1.treefield] = '';
        // this.handleSelectNode(node);  // 如果表格需要和树联动初始化时才需要
    }

    // ③ 事件处理函数
    handleSelectNode = async (node) => { ... }
    handleAddRow = () => { ... }
    handleEditRow = (row) => { ... }
    handleSaveRow = async () => { ... }
    handleDeleteRow = async () => { ... }

    // ④ render
    render() { ... }
}

1.3 关键问题:componentDidMount 为什么用 async?

// 不用 async/await 的写法(回调地狱,难以阅读)
componentDidMount() {
    reqdoSQL({ sqlprocedure: 'xxx' }).then(rs => {
        this.setState({ data: rs.rows });
    });
}

// 用 async/await 的写法(清晰线性)
componentDidMount = async () => {
    let rs = await reqdoSQL({ sqlprocedure: 'xxx' }); // 等待数据回来
    this.setState({ data: rs.rows });                  // 再更新
}

二、布局设计(Layout)

2.1 左树右表标准布局

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

render() {
    return (
        <Layout style=>

            {/* 左侧:分类树 */}
            <Sider
                theme='light'
                width={250}
                style=
            >
                <AntdTree
                    ref={ref => this.myTree1 = ref}
                    sqlprocedure="demo804a"
                    filterprocedure="demo804e"
                    loadstyle="expand"
                    root="全部商品分类"
                    filter="true"
                    icon={<PaperClipOutlined />}
                    blockNode={true}
                    onSelectNode={(key, e) => this.handleSelectNode(e.node)}
                />
            </Sider>

            {/* 右侧:数据表格 */}
            <Content>
                <AntdTable
                    ref={ref => this.myTable1 = ref}
                    columns={columns}
                    sqlprocedure="demo1101b"
                    categoryid=""
                    keyfield="productid"
                    keytitle="商品"
                    pagesize="20"
                    rownumber
                    toolbar="-;add;-;edit;-;delete;-;refresh"
                    onAddRow={() => this.handleAddRow()}
                    onEditRow={(row) => this.handleEditRow(row)}
                    onDeleteRow={() => this.handleDeleteRow()}
                    onSaveRow={() => this.handleSaveRow()}
                />
            </Content>
        </Layout>
    );
}

三、Tree 和 Table 联动(重点)

3.1 联动的完整代码

// 核心:点击树节点时,修改表格的筛选参数,重新加载数据
handleSelectNode = async (node) => {
    // 第一步:修改表格内部 attr 的 categoryid,并重置页码为 1
    this.myTable1?.setState({
        pageno: 1,
        attr: {
            ...this.myTable1.state.attr,           // 保留所有已有的表格属性
            categoryid: node[this.state.myTable1.treefield] || ''  // 更新分类ID
            //  node['subcategoryid'] 的值 → 如 'A1'
        }
    }, () => {
        // 第二步:等 setState 执行完后(通过回调),再让表格重新加载数据
        setTimeout(() => {
            this.myTable1?.loadTableData();
        });
    });
}

3.2 代码中三个”为什么”快速解答

问题答案
为什么要用 ...this.myTable1.state.attr展开保留原有所有配置,否则其他配置会被覆盖清空
为什么要 pageno: 1防止翻页后切换分类,新分类数据少页码越界导致空白
为什么要 setTimeoutsetState 是异步的,要等子组件状态更新后再触发 loadTableData

四、Form 表单设计

4.1 弹窗表单完整结构

render() {
    return (
        <>
            {/* 主界面布局(省略)*/}

            {/* 弹窗表单 */}
            <Modal
                title={this.state.addoredit === 'add' ? '新增商品' : '修改商品'}
                open={this.state.myWin1}    // 通过 state 控制显隐
                width={480}
                centered
                forceRender          // 强制预渲染,保证 ref 在关闭时不丢失
                maskClosable={false}
                keyboard={false}
                footer={[
                    <Button
                        key="save"
                        type="primary"
                        disabled={this.state.addoredit === 'query'}
                        onClick={this.handleSaveRow}
                    >
                        保存
                    </Button>,
                    <Button key="close" onClick={() => this.setState({ myWin1: false })}>
                        关闭
                    </Button>
                ]}
            >
                <Form name="myForm1" ref={ref => this.myForm1 = ref}>
                    <AntdInputBox
                        id="productid"
                        label="商品编号"
                        labelwidth="80"
                        width="200"
                        left="10"
                        top={8}
                        ref={ref => this.productid = ref}
                    />
                    <AntdInputBox
                        id="productname"
                        label="商品名称"
                        labelwidth="80"
                        width="320"
                        left="10"
                        top={8 + 42}    // 第二行 = 8 + 1个rowheight(42)
                        ref={ref => this.productname = ref}
                    />
                    {/* 更多字段... */}
                </Form>
            </Modal>
        </>
    );
}

4.2 CRUD 标准操作流程

// 新增
handleAddRow = () => {
    this.setState({ myWin1: true, addoredit: 'add' }, () => {
        setTimeout(() => {
            this.resetFormValues('myForm1');  // 清空表单
            this.productid.setState({ readOnly: false }); // 主键可编辑
        });
    });
}

// 修改
handleEditRow = (row) => {
    if (!row) row = this.myTable1.state.row; // 没传就取当前选中行
    this.setState({ myWin1: true, addoredit: 'update' }, () => {
        setTimeout(() => {
            this.setFormValues('myForm1', row);            // 填充数据到表单
            this.setFormFields('myForm1', 'readOnly', false); // 全部可编辑
            this.productid.setState({ readOnly: true });   // 主键不可修改
        });
    });
}

// 删除(MyFormComponent 内置方法,会弹出确认框)
handleDeleteRow = async () => {
    return await this.deleteTableRow(this.myTable1, 'myForm1', 'demo_del');
}

// 保存(MyFormComponent 内置方法,自动读表单数据 → 调存储过程)
handleSaveRow = async () => {
    let rs = await this.saveTableRow(this.myTable1, 'myForm1', 'demo_save');
    if (rs && rs.error === '') {
        this.setState({ myWin1: false }); // 保存成功,关闭弹窗
    }
}

五、存储过程设计规范

5.1 查询存储过程(带分页)

-- 必须返回 _total 列,供前端分页控件计算总页数
CREATE PROCEDURE demo_query(
    IN `$categoryid` VARCHAR(50),
    IN `$filter`     VARCHAR(255),
    IN `$pageno`     INT,
    IN `$pagesize`   INT
)
BEGIN
    DECLARE $start INT;
    SET $start = ($pageno - 1) * $pagesize;
    SET $filter = CONCAT('%', IFNULL($filter, ''), '%');

    SELECT *, COUNT(*) OVER() AS _total
    FROM products p
    LEFT JOIN suppliers s ON p.supplierid = s.supplierid
    WHERE ($categoryid = '' OR p.subcategoryid = $categoryid)
      AND (p.productname LIKE $filter OR p.productid LIKE $filter)
    ORDER BY p.productid
    LIMIT $start, $pagesize;
END;

5.2 保存存储过程(新增/修改二合一)

-- 必须返回 key 列值,供前端定位到保存的行
CREATE PROCEDURE demo_save(
    IN `$productid`   VARCHAR(20),
    IN `$productname` VARCHAR(100),
    IN `$unitprice`   DECIMAL(10,2),
    IN `$addoredit`   VARCHAR(10)   -- 'add' 或 'update'
)
BEGIN
    IF $addoredit = 'add' THEN
        INSERT INTO products(productid, productname, unitprice)
        VALUES($productid, $productname, $unitprice);
    ELSE
        UPDATE products
        SET productname = $productname, unitprice = $unitprice
        WHERE productid = $productid;
    END IF;

    -- 必须返回 key 字段,前端 saveTableRow 需要用到
    SELECT * FROM products WHERE productid = $productid;
END;

六、综合程序设计大题(模拟)

题目:请实现一个”供应商管理”页面,要求:

  1. 左侧有一个按”城市”分组的树(调用存储过程 sup_tree
  2. 右侧有一个供应商列表表格,包含编号、名称、城市、电话四列(调用 sup_list
  3. 点击左侧城市节点,右侧表格筛选显示该城市的供应商
  4. 表格顶部有新增、修改、删除按钮
  5. 点击新增或修改时弹出表单

参考答案框架(完整实现):

import React from 'react';
import { Layout, Button, Form, Modal } from 'antd';
import { MyFormComponent } from '../../api/antdFormMethod.js';
import { AntdTree } from '../../api/antdTrees';
import { AntdTable } from '../../api/antdTable.js';
import { AntdInputBox } from '../../api/antdClass.js';
import { PaperClipOutlined } from '@ant-design/icons';

const { Content, Sider } = Layout;
const sys = { ...React.sys };
const rowheight = 42;

// 表格列定义(放在类外面,静态不变的数据)
const columns = [
    { dataIndex: 'supplierid',   title: '供应商编号', width: '100px', align: 'center' },
    { dataIndex: 'suppliername', title: '供应商名称', width: '200px', ellipsis: true },
    { dataIndex: 'city',         title: '城市',       width: '100px', align: 'center' },
    { dataIndex: 'phone',        title: '联系电话',   width: '150px' },
];

export default class SupplierPage extends MyFormComponent {

    state = {
        myTable1: {
            rowindex: 0,
            keyfield: 'supplierid',
            treefield: 'city',    // 树节点关联字段(城市)
            lastrow: {},
        },
        myWin1: false,
        addoredit: 'update',
    }

    // 树节点点击联动表格
    handleSelectNode = async (node) => {
        this.myTable1?.setState({
            pageno: 1,
            attr: {
                ...this.myTable1.state.attr,
                city: node[this.state.myTable1.treefield] || ''
            }
        }, () => { setTimeout(() => { this.myTable1?.loadTableData(); }); });
    }

    // 新增
    handleAddRow = () => {
        this.setState({ myWin1: true, addoredit: 'add' }, () => {
            setTimeout(() => {
                this.resetFormValues('myForm1');
                this.supplierid.setState({ readOnly: false });
            });
        });
    }

    // 修改
    handleEditRow = (row) => {
        if (!row) row = this.myTable1.state.row;
        this.setState({ myWin1: true, addoredit: 'update' }, () => {
            setTimeout(() => {
                this.setFormValues('myForm1', row);
                this.setFormFields('myForm1', 'readOnly', false);
                this.supplierid.setState({ readOnly: true });
            });
        });
    }

    // 删除
    handleDeleteRow = async () => {
        return await this.deleteTableRow(this.myTable1, 'myForm1', 'sup_delete');
    }

    // 保存
    handleSaveRow = async () => {
        let rs = await this.saveTableRow(this.myTable1, 'myForm1', 'sup_save');
        if (rs && rs.error === '') this.setState({ myWin1: false });
    }

    render() {
        return (
            <>
                <Layout style=>
                    <Sider theme='light' width={220}
                        style=>
                        <AntdTree
                            ref={ref => this.myTree1 = ref}
                            sqlprocedure="sup_tree"
                            loadstyle="full"
                            root="全部城市"
                            filter="true"
                            icon={<PaperClipOutlined />}
                            blockNode={true}
                            onSelectNode={(key, e) => this.handleSelectNode(e.node)}
                        />
                    </Sider>
                    <Content>
                        <AntdTable
                            ref={ref => this.myTable1 = ref}
                            columns={columns}
                            sqlprocedure="sup_list"
                            city=""
                            keyfield="supplierid"
                            keytitle="供应商"
                            pagesize="20"
                            rownumber
                            toolbar="-;add;-;edit;-;delete;-;refresh"
                            onAddRow={() => this.handleAddRow()}
                            onEditRow={(row) => this.handleEditRow(row)}
                            onDeleteRow={() => this.handleDeleteRow()}
                        />
                    </Content>
                </Layout>

                {/* 弹窗 */}
                <Modal
                    title={this.state.addoredit === 'add' ? '新增供应商' : '修改供应商'}
                    open={this.state.myWin1}
                    width={440}
                    centered
                    forceRender
                    maskClosable={false}
                    footer={[
                        <Button key="save" type="primary"
                            disabled={this.state.addoredit === 'query'}
                            onClick={this.handleSaveRow}>保存</Button>,
                        <Button key="close"
                            onClick={() => this.setState({ myWin1: false })}>关闭</Button>
                    ]}
                >
                    <Form name="myForm1" ref={ref => this.myForm1 = ref}>
                        <AntdInputBox id="supplierid"   label="供应商编号" labelwidth="80"
                            width="200" left="10" top={8}
                            ref={ref => this.supplierid = ref} />
                        <AntdInputBox id="suppliername" label="供应商名称" labelwidth="80"
                            width="300" left="10" top={8 + rowheight}
                            ref={ref => this.suppliername = ref} />
                        <AntdInputBox id="city"         label="城市"     labelwidth="80"
                            width="200" left="10" top={8 + rowheight * 2}
                            ref={ref => this.city = ref} />
                        <AntdInputBox id="phone"        label="联系电话" labelwidth="80"
                            width="200" left="10" top={8 + rowheight * 3}
                            ref={ref => this.phone = ref} />
                    </Form>
                </Modal>
            </>
        );
    }
}

七、选择题高频考点整理

考点正确答案
ref 的作用让父组件直接获取子组件实例,调用其内部方法
setState 是否同步异步,修改后需在回调或 setTimeout 中操作
forceRender 在 Modal 中的作用强制预渲染弹窗内容,保证 ref 在关闭时不为 null
{...undefined} 的结果{} 空对象,不报错
componentDidMount 的执行时机DOM 挂载完成后执行一次
树联动表格时为什么 pageno: 1防止页码越界导致空表格
?. 可选链的作用对象为 null/undefined 时不报错,直接返回 undefined
columns 为什么不必放 statecolumns 是静态数据,不需要响应式,放类外常量更合理
保存存储过程必须返回什么保存的记录本身(包含主键),前端用于定位行
查询分页存储过程必须返回什么_total 列(总记录数)