主题
Day 7:样式配置:动态 className + CSS 变量
今天完成什么
- 做
StyleConfigForm组件 - 支持布局、尺寸、强调色配置
- 让 InfoCard 样式根据配置动态变化
接在昨天哪里
昨天做了字段显隐控制,config 里有三个样式字段还没用上:
layout:布局方式(纵向 / 紧凑)size:卡片尺寸(小 / 中)accentColor:强调色(状态标签背景色)
今天要让它们生效。
概念解释
动态 className
根据 props 或 state 动态拼接 class 名:
tsx
<div className={`card ${config.size === 'small' ? 'card-small' : 'card-medium'}`}>或者用对象:
tsx
<div className={`card card-${config.size} card-${config.layout}`}>更复杂的可以用工具函数:
tsx
function classNames(...classes: (string | false | undefined)[]) {
return classes.filter(Boolean).join(' ');
}
<div className={classNames('card', config.showBorder && 'card-border')}>CSS 变量(自定义属性)
CSS 变量能从 JS 动态注入值:
tsx
<div style={{ '--accent-color': config.accentColor }}>CSS 里引用:
css
.status {
background-color: var(--accent-color);
}这样不用每次都写 inline style,保持样式规则在 CSS。
为什么用 CSS 变量而不是直接 inline style
inline style 的问题:
tsx
<span style={{ backgroundColor: statusColors[config.statusType] }}>每次颜色变了,整个 style 对象都是新的,React 可能做多余的 DOM 更新。
CSS 变量更优雅:
tsx
<div style={{ '--status-color': statusColors[config.statusType] }}>
<span className="status">{config.statusText}</span>
</div>CSS 里统一管理样式,JS 只负责注入动态值。
动手实现
第 1 步:新建 StyleConfigForm
新建 src/features/infocard-config/StyleConfigForm.tsx:
tsx
import { InfoCardConfig } from '../../types/config';
type StyleConfigFormProps = {
config: InfoCardConfig;
onChange: (config: InfoCardConfig) => void;
};
function StyleConfigForm({ config, onChange }: StyleConfigFormProps) {
return (
<div>
<h3>样式设置</h3>
{/* 布局 */}
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px' }}>
布局方式
</label>
<select
value={config.layout}
onChange={(e) => onChange({
...config,
layout: e.target.value as InfoCardConfig['layout']
})}
style={{
width: '100%',
padding: '6px 8px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px'
}}
>
<option value="vertical">纵向(标准)</option>
<option value="compact">紧凑(横向)</option>
</select>
</div>
{/* 尺寸 */}
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px' }}>
卡片尺寸
</label>
<select
value={config.size}
onChange={(e) => onChange({
...config,
size: e.target.value as InfoCardConfig['size']
})}
style={{
width: '100%',
padding: '6px 8px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px'
}}
>
<option value="small">小</option>
<option value="medium">中</option>
</select>
</div>
{/* 强调色 */}
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '4px', fontSize: '14px' }}>
强调色
</label>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<input
type="color"
value={config.accentColor}
onChange={(e) => onChange({ ...config, accentColor: e.target.value })}
style={{
width: '50px',
height: '32px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
cursor: 'pointer'
}}
/>
<input
type="text"
value={config.accentColor}
onChange={(e) => onChange({ ...config, accentColor: e.target.value })}
placeholder="#52c41a"
style={{
flex: 1,
padding: '6px 8px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
fontSize: '14px'
}}
/>
</div>
</div>
</div>
);
}
export default StyleConfigForm;第 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 { InfoCardConfig } from '../types/config';
type ConfigPanelProps = {
config: InfoCardConfig;
onChange: (config: InfoCardConfig) => void;
};
function ConfigPanel({ config, onChange }: ConfigPanelProps) {
return (
<div style={{
flex: '1',
padding: '20px',
overflowY: 'auto',
borderRight: '1px solid #ddd'
}}>
<h2 style={{ marginTop: 0 }}>配置面板</h2>
<BasicConfigForm config={config} onChange={onChange} />
<hr style={{ margin: '24px 0', border: 'none', borderTop: '1px solid #e0e0e0' }} />
<FieldConfigList config={config} onChange={onChange} />
<hr style={{ margin: '24px 0', border: 'none', borderTop: '1px solid #e0e0e0' }} />
<StyleConfigForm config={config} onChange={onChange} />
</div>
);
}
export default ConfigPanel;第 3 步:让 InfoCardPreview 响应样式配置
修改 src/components/InfoCardPreview.tsx:
tsx
import { InfoCardConfig } from '../types/config';
type InfoCardPreviewProps = {
config: InfoCardConfig;
};
function InfoCardPreview({ config }: InfoCardPreviewProps) {
const statusColors = {
default: '#d9d9d9',
success: '#52c41a',
warning: '#faad14',
danger: '#f5222d'
};
const statusBgColor = statusColors[config.statusType];
// 根据 layout 和 size 决定样式
const isCompact = config.layout === 'compact';
const isSmall = config.size === 'small';
const cardPadding = isSmall ? '12px' : '16px';
const titleSize = isSmall ? '16px' : '18px';
const textSize = isSmall ? '13px' : '14px';
return (
<div
style={{
backgroundColor: '#fff',
border: config.showBorder ? '1px solid #e0e0e0' : 'none',
borderRadius: '8px',
padding: cardPadding,
maxWidth: '400px',
// CSS 变量注入
['--accent-color' as any]: config.accentColor
}}
>
{/* 标题栏 */}
<div style={{
display: 'flex',
flexDirection: isCompact ? 'row' : 'column',
justifyContent: isCompact ? 'space-between' : 'flex-start',
alignItems: isCompact ? 'center' : 'flex-start',
marginBottom: '12px',
gap: isCompact ? '12px' : '4px'
}}>
<div style={{ flex: isCompact ? 1 : 'none' }}>
<h3 style={{ margin: 0, fontSize: titleSize }}>{config.title}</h3>
{!isCompact && (
<p style={{ margin: '4px 0 0', color: '#666', fontSize: textSize }}>
{config.subtitle}
</p>
)}
</div>
<span style={{
padding: isSmall ? '2px 8px' : '4px 12px',
backgroundColor: statusBgColor,
color: '#fff',
borderRadius: '4px',
fontSize: isSmall ? '11px' : '12px',
whiteSpace: 'nowrap'
}}>
{config.statusText}
</span>
</div>
{/* 紧凑模式下副标题放到字段前面 */}
{isCompact && config.subtitle && (
<p style={{ margin: '0 0 8px', color: '#666', fontSize: textSize }}>
{config.subtitle}
</p>
)}
{/* 字段列表 */}
{config.fields.filter(f => f.visible).length > 0 && (
<div style={{
borderTop: '1px solid #f0f0f0',
paddingTop: '12px',
marginBottom: '12px'
}}>
{config.fields
.filter(field => field.visible)
.map(field => (
<div key={field.id} style={{ marginBottom: '8px' }}>
<span style={{ color: '#999', fontSize: textSize }}>{field.label}:</span>
<span style={{ fontSize: textSize }}>{field.value}</span>
</div>
))}
</div>
)}
{/* 操作按钮 */}
{config.actions.filter(a => a.visible).length > 0 && (
<div style={{ display: 'flex', gap: '8px' }}>
{config.actions
.filter(action => action.visible)
.map(action => (
<button
key={action.id}
style={{
padding: isSmall ? '4px 12px' : '6px 16px',
backgroundColor: action.type === 'primary' ? '#1890ff' : '#fff',
color: action.type === 'primary' ? '#fff' : '#333',
border: action.type === 'primary' ? 'none' : '1px solid #d9d9d9',
borderRadius: '4px',
cursor: 'pointer',
fontSize: textSize
}}
onClick={() => alert(`点击了:${action.text}`)}
>
{action.text}
</button>
))}
</div>
)}
</div>
);
}
export default InfoCardPreview;运行效果
保存后刷新:
- 配置面板最下方出现「样式设置」区域
- 切换「布局方式」为「紧凑」,卡片变成横向布局
- 切换「卡片尺寸」为「小」,字体和间距变小
- 点颜色选择器或输入框修改强调色,状态标签颜色跟着变
常见错误
1. CSS 变量名没加引号
tsx
// ❌ 错误
style={{ --accent-color: color }} // 语法错误
// ✅ 正确
style={{ '--accent-color': color }}
// 或者用类型断言
style={{ ['--accent-color' as any]: color }}2. 动态 className 拼接时有空格问题
tsx
// ❌ 可能产生 "card card-small" 或 "card " 多余空格
className={`card ${config.size === 'small' ? 'card-small' : ''}`}
// ✅ 用 filter 去掉空字符串
className={['card', config.size === 'small' && 'card-small'].filter(Boolean).join(' ')}3. 颜色输入框没同步
tsx
// ❌ 一个改了另一个不跟
<input type="color" value={color} onChange={...} />
<input type="text" value={anotherColor} onChange={...} />
// ✅ 都绑定同一个 state
<input type="color" value={config.accentColor} onChange={(e) => onChange({ ...config, accentColor: e.target.value })} />
<input type="text" value={config.accentColor} onChange={(e) => onChange({ ...config, accentColor: e.target.value })} />动手改一改
- 加一个「卡片圆角」配置(
0px/4px/8px/16px) - 在
InfoCardConfig里加borderRadius字段 - 在
StyleConfigForm里加下拉框 - 在
InfoCardPreview里应用borderRadius
验收清单
- [ ] 能看到「样式设置」区域
- [ ] 切换布局,卡片在纵向 / 横向之间切换
- [ ] 切换尺寸,卡片字体和间距变化
- [ ] 修改强调色,状态标签颜色实时更新
- [ ] 能解释动态 className 的写法
- [ ] 能解释 CSS 变量的用法
- [ ] 完成「动手改一改」的练习
今日记录
今天跑通:
- 样式配置表单
- 动态 className
- CSS 变量注入
- 条件判断控制布局
现在能解释:
- 为什么用 CSS 变量而不是直接 inline style
- 动态 className 的几种拼接方式
明天先做:
- 操作按钮配置(复用字段列表的逻辑)
- 按钮类型切换
留给明天的接口
config.actions 数组目前是写死的,和 fields 一样需要增删改。
明天要做 ActionConfigList,逻辑和 FieldConfigList 几乎一样。
进行中