软开(二):React高级Component
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 自定义属性传入自定义组件
AntdTree、AntdTable 等自定义组件通过 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 输入框组件
使用绝对定位,通过 top、left 精确控制位置:
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 是被点击的节点对象,包含所有字段
/>