零 UI 库依赖,3 列基础示例,使用原生 <input> 元素渲染每列。
[]
import { useState } from 'react';
import { CascadingInput } from 'react-cascading-input';
import 'react-cascading-input/styles';
import type { ColumnConfig } from 'react-cascading-input';
const columns: ColumnConfig[] = [
{
title: '训练任务',
dataIndex: 'product',
width: 120,
hasAdd: true,
render: ({ value, onChange, title }) => (
<input
style={{ width: '100%', boxSizing: 'border-box', padding: '4px 8px',
border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14, height: 32 }}
placeholder={`请输入${title}`}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
),
},
{
title: '训练集群',
dataIndex: 'region',
width: 120,
hasAdd: true,
render: ({ value, onChange, title }) => (
<input
style={{ width: '100%', boxSizing: 'border-box', padding: '4px 8px',
border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14, height: 32 }}
placeholder={`请输入${title}`}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
),
},
{
title: '框架版本',
dataIndex: 'spec',
width: 180,
hasAdd: false,
render: ({ value, onChange, title }) => (
<input
style={{ width: '100%', boxSizing: 'border-box', padding: '4px 8px',
border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14, height: 32 }}
placeholder={`请输入${title}`}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
),
},
];
export function App() {
const [value, setValue] = useState<any[]>([]);
return <CascadingInput columns={columns} value={value} onChange={setValue} />;
}同一组件中混合使用 <input> 和 <select>,展示 render prop 的灵活性。4 层嵌套,训练集群和框架版本使用下拉选择。同时演示了 level、isLeaf、node 等 CellRenderProps 参数的实际用法:通过 level 给不同层级设置不同边框色,通过 node.children 显示子节点数量,通过 isLeaf 给末列加浅灰背景。
[]
import { useState } from 'react';
import { CascadingInput } from 'react-cascading-input';
import 'react-cascading-input/styles';
import type { ColumnConfig } from 'react-cascading-input';
const selectStyle: React.CSSProperties = {
width: '100%', boxSizing: 'border-box', padding: '4px 8px',
border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14, height: 32, background: '#fff',
};
const inputStyle: React.CSSProperties = {
width: '100%', boxSizing: 'border-box', padding: '4px 8px',
border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14, height: 32,
};
/** 不同层级的边框色,通过 level 区分视觉深度 */
const levelColors = ['#d9d9d9', '#91d5ff', '#b7eb8f', '#ffd591'];
const columns: ColumnConfig[] = [
{
title: '训练任务',
dataIndex: 'product',
width: 120,
hasAdd: true,
render: ({ value, onChange, title, level }) => (
<input
style={{ ...inputStyle, borderColor: levelColors[level] || '#d9d9d9' }}
placeholder={`请输入${title}`}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
),
},
{
title: '训练集群',
dataIndex: 'region',
width: 130,
hasAdd: true,
render: ({ value, onChange, title, level, node }) => (
<div style={{ position: 'relative' }}>
<select
style={{ ...selectStyle, borderColor: levelColors[level] || '#d9d9d9' }}
value={value || ''}
onChange={(e) => onChange(e.target.value)}
>
<option value="">请选择{title}</option>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
<option value="guangzhou">广州</option>
</select>
{node.children && node.children.length > 0 && (
<span style={{ position: 'absolute', right: 24, top: 8, fontSize: 11, color: '#999', pointerEvents: 'none' }}>
{node.children.length}项
</span>
)}
</div>
),
},
{
title: '框架版本',
dataIndex: 'framework',
width: 130,
hasAdd: true,
render: ({ value, onChange, title, level }) => (
<select
style={{ ...selectStyle, borderColor: levelColors[level] || '#d9d9d9' }}
value={value || ''}
onChange={(e) => onChange(e.target.value)}
>
<option value="">请选择{title}</option>
<option value="pytorch">PyTorch</option>
<option value="tensorflow">TensorFlow</option>
<option value="mindspore">MindSpore</option>
</select>
),
},
{
title: '训练阶段',
dataIndex: 'stage',
width: 120,
hasAdd: false,
render: ({ value, onChange, title, level, isLeaf }) => (
<select
style={{
...selectStyle,
borderColor: levelColors[level] || '#d9d9d9',
background: isLeaf ? '#fafafa' : '#fff',
}}
value={value || ''}
onChange={(e) => onChange(e.target.value)}
>
<option value="">请选择{title}</option>
<option value="preprocess">预处理</option>
<option value="training">训练中</option>
<option value="eval">评估</option>
</select>
),
},
];
export function App() {
const [value, setValue] = useState<any[]>([]);
return <CascadingInput columns={columns} value={value} onChange={setValue} />;
}5 列层级嵌套,验证组件在更深层级下的连线表现和数据管理。
[]
import { useState } from 'react';
import { CascadingInput } from 'react-cascading-input';
import 'react-cascading-input/styles';
import type { ColumnConfig } from 'react-cascading-input';
const inputStyle: React.CSSProperties = {
width: '100%', boxSizing: 'border-box', padding: '4px 8px',
border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14, height: 32,
};
const mkInput = (title: string, dataIndex: string, width: number, hasAdd: boolean): ColumnConfig => ({
title, dataIndex, width, hasAdd,
render: ({ value, onChange }) => (
<input style={inputStyle} placeholder={`请输入${title}`} value={value} onChange={(e) => onChange(e.target.value)} />
),
});
const columns: ColumnConfig[] = [
mkInput('项目', 'project', 100, true),
mkInput('环境', 'env', 100, true),
mkInput('服务', 'service', 100, true),
mkInput('实例', 'instance', 100, true),
mkInput('配置', 'config', 120, false),
];
export function App() {
const [value, setValue] = useState<any[]>([]);
return <CascadingInput columns={columns} value={value} onChange={setValue} />;
}通过 addRender 和 deleteRender 自定义"添加"和"删除"按钮的样式,按钮位置由组件固定控制(添加在单元格下方,删除在行末尾)。
[]
import { useState } from 'react';
import { CascadingInput } from 'react-cascading-input';
import 'react-cascading-input/styles';
import type { ColumnConfig } from 'react-cascading-input';
const addBtnStyle: React.CSSProperties = {
padding: '2px 12px', border: '1px solid #1890ff', borderRadius: 4,
background: '#e6f7ff', color: '#1890ff', fontSize: 12, cursor: 'pointer',
};
const deleteBtnStyle: React.CSSProperties = {
padding: '2px 12px', border: '1px solid #ff4d4f', borderRadius: 4,
background: '#fff1f0', color: '#ff4d4f', fontSize: 12, cursor: 'pointer',
};
const columns: ColumnConfig[] = [
{
title: '训练任务',
dataIndex: 'product',
width: 120,
hasAdd: true,
addRender: ({ onClick }) => (
<button type="button" style={addBtnStyle} onClick={onClick}>+ 添加</button>
),
deleteRender: ({ onClick }) => (
<button type="button" style={deleteBtnStyle} onClick={onClick}>删除</button>
),
render: ({ value, onChange, title }) => (
<input
style={{ width: '100%', padding: '4px 8px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14, height: 32 }}
placeholder={`请输入${title}`}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
),
},
{
title: '训练集群',
dataIndex: 'region',
width: 120,
hasAdd: true,
addRender: ({ onClick }) => (
<button type="button" style={addBtnStyle} onClick={onClick}>+ 添加</button>
),
deleteRender: ({ onClick }) => (
<button type="button" style={deleteBtnStyle} onClick={onClick}>删除</button>
),
render: ({ value, onChange, title }) => (
<input
style={{ width: '100%', padding: '4px 8px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14, height: 32 }}
placeholder={`请输入${title}`}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
),
},
{
title: '框架版本',
dataIndex: 'spec',
width: 180,
hasAdd: false,
deleteRender: ({ onClick }) => (
<button type="button" style={deleteBtnStyle} onClick={onClick}>删除</button>
),
render: ({ value, onChange, title }) => (
<input
style={{ width: '100%', padding: '4px 8px', border: '1px solid #d9d9d9', borderRadius: 4, fontSize: 14, height: 32 }}
placeholder={`请输入${title}`}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
),
},
];
export function App() {
const [value, setValue] = useState<any[]>([]);
return <CascadingInput columns={columns} value={value} onChange={setValue} />;
}实时切换连线样式(贝塞尔曲线 / 折线)、颜色、粗细,以及开启溯源动画(线条上粒子从子节点流向父节点)。
[]
import { useState } from 'react';
import { CascadingInput } from 'react-cascading-input';
import 'react-cascading-input/styles';
import type { ColumnConfig } from 'react-cascading-input';
const columns: ColumnConfig[] = [
{
title: '训练任务', dataIndex: 'product', width: 120, hasAdd: true,
render: ({ value, onChange }) => (
<input placeholder="请输入训练任务" value={value} onChange={(e) => onChange(e.target.value)} />
),
},
{
title: '训练集群', dataIndex: 'region', width: 120, hasAdd: true,
render: ({ value, onChange }) => (
<input placeholder="请输入训练集群" value={value} onChange={(e) => onChange(e.target.value)} />
),
},
{
title: '框架版本', dataIndex: 'spec', width: 180, hasAdd: false,
render: ({ value, onChange }) => (
<input placeholder="请输入框架版本" value={value} onChange={(e) => onChange(e.target.value)} />
),
},
];
export function App() {
const [value, setValue] = useState<any[]>([]);
const [lineStyle, setLineStyle] = useState<'curve' | 'straight'>('curve');
const [lineColor, setLineColor] = useState('#d9d9d9');
const [lineWidth, setLineWidth] = useState(1.5);
const [showSource, setShowSource] = useState(false);
const line = { style: lineStyle, color: lineColor, width: lineWidth, showSource };
return (
<>
<div style={{ marginBottom: 16, display: 'flex', gap: 16, alignItems: 'center' }}>
<label>
连线样式:
<select value={lineStyle} onChange={(e) => setLineStyle(e.target.value as 'curve' | 'straight')}>
<option value="curve">贝塞尔曲线</option>
<option value="straight">折线</option>
</select>
</label>
<label>
连线颜色:
<input type="color" value={lineColor} onChange={(e) => setLineColor(e.target.value)} />
</label>
<label>
连线粗细:
<input type="range" min={0.5} max={4} step={0.5} value={lineWidth}
onChange={(e) => setLineWidth(Number(e.target.value))} />
</label>
<label>
<input type="checkbox" checked={showSource} onChange={(e) => setShowSource(e.target.checked)} />
溯源动画
</label>
</div>
<CascadingInput
columns={columns}
value={value}
onChange={setValue}
line={line}
/>
</>
);
}