软开(六):例题(二)
期末复习资料(二):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? | 防止翻页后切换分类,新分类数据少页码越界导致空白 |
为什么要 setTimeout? | setState 是异步的,要等子组件状态更新后再触发 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;
六、综合程序设计大题(模拟)
题目:请实现一个”供应商管理”页面,要求:
- 左侧有一个按”城市”分组的树(调用存储过程
sup_tree) - 右侧有一个供应商列表表格,包含编号、名称、城市、电话四列(调用
sup_list) - 点击左侧城市节点,右侧表格筛选显示该城市的供应商
- 表格顶部有新增、修改、删除按钮
- 点击新增或修改时弹出表单
参考答案框架(完整实现):
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 为什么不必放 state | columns 是静态数据,不需要响应式,放类外常量更合理 |
| 保存存储过程必须返回什么 | 保存的记录本身(包含主键),前端用于定位行 |
| 查询分页存储过程必须返回什么 | _total 列(总记录数) |