主题
Day 8:操作按钮配置
今天完成什么
- 做
ActionConfigList组件 - 支持按钮的增删改、显隐、类型切换
- 复用昨天字段列表的思路
接在昨天哪里
昨天做了样式配置,今天的逻辑和 Day 5(字段列表)几乎一样,config.actions 数组也需要增删改。
这一天的重点不是新知识,而是验证你真正理解了数组操作,能独立复用之前的模式。
概念解释
组件复用的思路
昨天写了 FieldConfigList,今天的 ActionConfigList 结构几乎相同:
- 都是数组渲染
- 都有增删改
- 都有显隐控制
不同点只是:
- 按钮有
type字段(primary/default),字段没有 - 按钮的展示逻辑稍有不同
遇到类似结构时,先想能不能复用,不能复用再单独写。
Partial<T> 类型工具
昨天用到了 Partial<InfoCardField>,今天再看一眼:
tsx
type InfoCardField = {
id: string;
label: string;
value: string;
visible: boolean;
};
// Partial 让所有字段变成可选
type UpdateField = Partial<InfoCardField>;
// 等价于:
// { id?: string; label?: string; value?: string; visible?: boolean }这样 handleUpdateField(id, { label: 'new' }) 只传要改的字段就行,不用把整个对象传过去。
动手实现
第 1 步:新建 ActionConfigList
新建 src/features/infocard-config/ActionConfigList.tsx:
tsx
import { InfoCardAction, InfoCardConfig } from '../../types/config';
type ActionConfigListProps = {
config: InfoCardConfig;
onChange: (config: InfoCardConfig) => void;
};
function ActionConfigList({ config, onChange }: ActionConfigListProps) {
const handleAddAction = () => {
const newAction: InfoCardAction = {
id: `action_${Date.now()}`,
text: '新按钮',
type: 'default',
visible: true
};
onChange({
...config,
actions: [...config.actions, newAction]
});
};
const handleDeleteAction = (id: string) => {
onChange({
...config,
actions: config.actions.filter(a => a.id !== id)
});
};
const handleUpdateAction = (id: string, updates: Partial<InfoCardAction>) => {
onChange({
...config,
actions: config.actions.map(a => a.id === id ? { ...a, ...updates } : a)
});
};
return (
<div>
<h3>操作按钮</h3>
{config.actions.map(action => (
<div
key={action.id}
style={{
border: '1px solid #e0e0e0',
borderRadius: '4px',
padding: '12px',
marginBottom: '12px',
backgroundColor: action.visible ? '#fafafa' : '#f0f0f0',
opacity: action.visible ? 1 : 0.6
}}
>
{/* 顶部:显隐 + 删除 */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '10px'
}}>
<label style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '14px' }}>
<input
type="checkbox"
checked={action.visible}
onChange={(e) => handleUpdateAction(action.id, { visible: e.target.checked })}
/>
显示
</label>
<button
onClick={() => handleDeleteAction(action.id)}
style={{
padding: '2px 10px',
backgroundColor: '#ff4d4f',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px'
}}
>
删除
</button>
</div>
{/* 按钮文案 */}
<div style={{ marginBottom: '8px' }}>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', color: '#666' }}>
按钮文案
</label>
<input
type="text"
value={action.text}
onChange={(e) => handleUpdateAction(action.id, { text: e.target.value })}
style={{
width: '100%',
padding: '4px 8px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px',
boxSizing: 'border-box'
}}
/>
</div>
{/* 按钮类型 */}
<div>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px', color: '#666' }}>
按钮类型
</label>
<select
value={action.type}
onChange={(e) => handleUpdateAction(action.id, {
type: e.target.value as InfoCardAction['type']
})}
style={{
width: '100%',
padding: '4px 8px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px'
}}
>
<option value="primary">主要(蓝色实心)</option>
<option value="default">默认(白色边框)</option>
</select>
</div>
</div>
))}
<button
onClick={handleAddAction}
style={{
padding: '6px 16px',
backgroundColor: '#1890ff',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
+ 新增按钮
</button>
</div>
);
}
export default ActionConfigList;第 2 步:在 ConfigPanel 里加上按钮配置
修改 src/components/ConfigPanel.tsx:
tsx
import BasicConfigForm from '../features/infocard-config/BasicConfigForm';
import FieldConfigList from '../features/infocard-config/FieldConfigList';
import StyleConfigForm from '../features/infocard-config/StyleConfigForm';
import ActionConfigList from '../features/infocard-config/ActionConfigList';
import { InfoCardConfig } from '../types/config';
type ConfigPanelProps = {
config: InfoCardConfig;
onChange: (config: InfoCardConfig) => void;
};
function ConfigPanel({ config, onChange }: ConfigPanelProps) {
const divider = (
<hr style={{ margin: '24px 0', border: 'none', borderTop: '1px solid #e0e0e0' }} />
);
return (
<div style={{
flex: '1',
padding: '20px',
overflowY: 'auto',
borderRight: '1px solid #ddd'
}}>
<h2 style={{ marginTop: 0 }}>配置面板</h2>
<BasicConfigForm config={config} onChange={onChange} />
{divider}
<FieldConfigList config={config} onChange={onChange} />
{divider}
<StyleConfigForm config={config} onChange={onChange} />
{divider}
<ActionConfigList config={config} onChange={onChange} />
</div>
);
}
export default ConfigPanel;当前目录结构
text
src/
├─ types/config.ts
├─ components/
│ ├─ LowCodeConfigPage.tsx
│ ├─ ComponentSidebar.tsx
│ ├─ ConfigPanel.tsx
│ ├─ PreviewPanel.tsx
│ └─ InfoCardPreview.tsx
└─ features/
└─ infocard-config/
├─ BasicConfigForm.tsx
├─ FieldConfigList.tsx
├─ StyleConfigForm.tsx
└─ ActionConfigList.tsx运行效果
保存后刷新:
- 配置面板底部出现「操作按钮」区域
- 点「+ 新增按钮」,右侧卡片出现新按钮
- 修改按钮文案,右侧实时更新
- 切换类型为「主要」,按钮变蓝色实心
- 取消显示,按钮消失
常见错误
参考 Day 5 的错误清单,数组操作的坑都一样:
- 不要直接 push / splice
- map 修改时不要漏掉
: action - key 不要用索引
这里只补一个新坑:
select 类型断言不能省
tsx
// ❌ 类型不匹配,TS 会报错
onChange({ ...config, type: e.target.value });
// ✅ 正确
onChange({ ...config, type: e.target.value as InfoCardAction['type'] });动手改一改
- 给按钮加一个「点击行为」字段:
alert(弹窗)/log(控制台)/copy(复制文案) - 在
InfoCardPreview里根据这个字段执行不同的点击动作
验收清单
- [ ] 能看到「操作按钮」区域
- [ ] 点「+ 新增按钮」,右侧出现新按钮
- [ ] 修改按钮文案,右侧实时更新
- [ ] 切换类型,按钮样式变化
- [ ] 取消显示,右侧按钮消失
- [ ] 完成「动手改一改」的练习
今日记录
今天跑通:
- ActionConfigList 的增删改
- 复用了 FieldConfigList 的模式
- ConfigPanel 四个区域全部完成
现在能解释:
- 为什么
Partial<T>很有用 - 类似结构时如何复用逻辑
明天先做:
- JSON 预览
useMemo避免重复计算
留给明天的接口
现在 config 已经能被完整配置了,下一步要在右侧下方加一个区域,实时显示当前配置的 JSON。
进行中