在实际项目中,起初采用的是 Element Plus 提供的 ElSteps 组件,但随着业务的不断更新,原始组件已经无法满足需要,最终选择手写更贴合业务需求的步骤条组件。
比如,有如下需求的步骤条,第一反应可能是基于现有的 ElSteps 组件进行样式调整,然而在实际尝试过程中发现,如果想实现诸如“将标题在序号后展示”这样的效果,会大量依赖 position 定位等样式,不仅实现复杂,而且后期维护成本很高。因此,放弃对现有组件的改造,转而手写一个。
简单实现,手写一个横向滚动条,垂直滚动条仍沿用 ElSteps。
const Steps = defineComponent({
name: 'RBSteps',
props: {
...ElSteps.props,
// 步骤数据
steps: {
type: Array,
default: () => [],
},
// 当前激活步骤
active: {
type: Number,
default: 0,
},
// 方向
direction: {
type: String,
default: StepBarType.horizontal,
validator: (value) => [StepBarType.horizontal, StepBarType.vertical].includes(value),
}
},
setup(props, { attrs, slots, expose }) {
const stepsRef = ref<InstanceType<typeof ElSteps>>();
expose({
getStepsInstance: () => stepsRef.value,
});
return () => (
<div class="business-steps-common">
{props.direction === StepBarType.horizontal ? (
<div class='steps-horizontal'>
{props.steps?.map((step, index) => {
step.status = step.status || StepBarStatus.default;
return (
<div class='option'>
<div class='step'>
{/* 步骤序号圆圈 */}
<div class={`circle ${step.status}`}>
{[StepBarStatus.default, StepBarStatus.process].includes(step.status) ? (index + 1) : ''}
</div>
{/* 标题和描述 */}
<div class='content' style='padding: 0'>
<div class={`title ${step.status}`}>{step.title}</div>
<div class={`desc ${step.status}`}>{step.desc}</div>
</div>
</div>
{/* 连接线 */}
{index < props.steps.length - 1 && (
<div class='line' style={{ width: `calc(100% - 6rem)` }}></div>
)}
</div>
)
})}
</div>
) : (
<ElSteps
ref={stepsRef}
v-slots={slots}
{...props}
{...attrs}
/>
)}
</div>
)
},
})
由此可以实现,不同的状态下的步骤条展示。
好处是什么呢?
手写步骤条的最大好处之一在于状态控制的灵活性。在自定义的组件中,每一步的状态可以完全由外层组件进行控制——比如某一步是否激活、是否已完成、是否需要回退,甚至可以根据异步请求的结果或复杂的业务逻辑动态更新。这种能力让步骤条不仅是一个“展示组件”,更具备了“业务状态驱动”的能力,使得整个流程的掌控更精细,也更符合真实的业务场景。
比如:
export default defineComponent({
name: "Status",
setup() {
const steps_dynamic = ref<StepsType[]>([
{ title: '步骤一', desc: '这里是提示文字,这里是提示文字这里是提示文字' },
{ title: '步骤二', desc: '这里是提示文字,这里是提示文字这里是提示文字' },
{ title: '步骤三', desc: '这里是提示文字,这里是提示文字这里是提示文字' },
{ title: '步骤四', desc: '这里是提示文字,这里是提示文字这里是提示文字' },
]);
const activeStep = ref(0);
const statusMap = {
0: 'success',
1: 'error',
2: 'process',
3: 'default',
};
const handlePrev = () => {
if (activeStep.value === 0) {
MyMessage.warning('当前已经是第一步');
return;
}
activeStep.value--;
steps_dynamic.value[activeStep.value].status = 'default';
}
const handleNext = async () => {
const currentStep = activeStep.value;
if (currentStep >= steps_dynamic.value.length) {
MyMessage.info('到达最后一步');
return;
}
const newStatus = statusMap[currentStep];
if (newStatus) {
steps_dynamic.value[currentStep].status = newStatus;
activeStep.value++;
if (activeStep.value === steps_dynamic.value.length) {
MyMessage.warning('当前状态是 default,没有样式变化');
}
} else {
steps_dynamic.value[currentStep].status = 'default';
}
};
return () => (
<div class={styles.steps_wrapper}>
<MySteps
active={activeStep.value}
steps={steps_dynamic.value}
direction="horizontal"
/>
<div class={styles.btn_container}>
<MyButton type="info" plain size='small' onClick={handlePrev}>上一步</MyButton>
<MyButton type="info" plain size='small' onClick={handleNext}>下一步</MyButton>
</div>
</div>
);
}
});
每点击一步,就会执行一步。
展示如下:
需要注意 📢
当前横向步骤条实现很简单,若需扩展功能,可根据实际需求自主实现。
总结
手写一个步骤条组件,也许比我们想象中花的时间要多,但它带来的不仅是一个“符合需求的 UI 组件”,更是一次深刻的技术锤炼和设计思维的进化。
当我们从“拿来即用”走向“定制打造”,我们不只是工程师,更是构建体验的设计者。