Skip to content

Commit 748ceca

Browse files
feat: add fu-step (#12562)
1 parent b783034 commit 748ceca

13 files changed

Lines changed: 622 additions & 279 deletions
Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1 @@
1-
import { defineComponent, h } from 'vue';
2-
3-
export default defineComponent({
4-
name: 'FuStep',
5-
props: {
6-
id: {
7-
type: String,
8-
default: '',
9-
},
10-
title: {
11-
type: String,
12-
default: '',
13-
},
14-
},
15-
setup(_props, { slots }) {
16-
return () => h('div', slots.default?.());
17-
},
18-
});
1+
export { default } from './steps/FuStep.vue';
Lines changed: 1 addition & 213 deletions
Original file line numberDiff line numberDiff line change
@@ -1,213 +1 @@
1-
import { computed, defineComponent, h, ref, watch, type PropType, type VNode } from 'vue';
2-
3-
import { flattenVNodes, getVNodeComponentName } from './shared';
4-
5-
interface FuStepItem {
6-
id: string;
7-
index: number;
8-
title: string;
9-
vnode: VNode;
10-
}
11-
12-
const buildStepChildren = (vnode: VNode) => {
13-
const children = vnode.children as Record<string, (() => VNode[]) | undefined> | null;
14-
return children?.default?.() || [];
15-
};
16-
17-
export default defineComponent({
18-
name: 'FuSteps',
19-
props: {
20-
active: {
21-
type: Number,
22-
default: 0,
23-
},
24-
direction: {
25-
type: String,
26-
default: 'horizontal',
27-
},
28-
space: {
29-
type: Number,
30-
default: 0,
31-
},
32-
isLoading: {
33-
type: Boolean,
34-
default: false,
35-
},
36-
finishButtonText: {
37-
type: String,
38-
default: '',
39-
},
40-
beforeLeave: {
41-
type: Function as PropType<
42-
(step: { id: string; index: number; title: string }) => boolean | Promise<boolean>
43-
>,
44-
default: undefined,
45-
},
46-
},
47-
emits: ['change'],
48-
setup(props, { emit, slots, expose }) {
49-
const activeIndex = ref(props.active);
50-
const isRunningBeforeLeave = ref(false);
51-
52-
const stepItems = computed<FuStepItem[]>(() =>
53-
flattenVNodes(slots.default?.() || [])
54-
.filter((vnode) => getVNodeComponentName(vnode) === 'FuStep')
55-
.map((vnode, index) => {
56-
const vnodeProps = (vnode.props || {}) as Record<string, any>;
57-
return {
58-
id: String(vnodeProps.id || index),
59-
index,
60-
title: String(vnodeProps.title || ''),
61-
vnode,
62-
};
63-
}),
64-
);
65-
66-
const emitChange = () => {
67-
const currentStep = stepItems.value[activeIndex.value];
68-
if (!currentStep) {
69-
return;
70-
}
71-
emit('change', {
72-
id: currentStep.id,
73-
index: currentStep.index,
74-
title: currentStep.title,
75-
});
76-
};
77-
78-
watch(
79-
() => props.active,
80-
(value) => {
81-
activeIndex.value = value;
82-
},
83-
);
84-
85-
watch(
86-
stepItems,
87-
(items) => {
88-
if (items.length === 0) {
89-
activeIndex.value = 0;
90-
return;
91-
}
92-
if (activeIndex.value > items.length - 1) {
93-
activeIndex.value = items.length - 1;
94-
}
95-
emitChange();
96-
},
97-
{ immediate: true, deep: true },
98-
);
99-
100-
const runBeforeLeave = async () => {
101-
if (!props.beforeLeave) {
102-
return true;
103-
}
104-
const currentStep = stepItems.value[activeIndex.value];
105-
if (!currentStep) {
106-
return true;
107-
}
108-
if (isRunningBeforeLeave.value) {
109-
return true;
110-
}
111-
isRunningBeforeLeave.value = true;
112-
try {
113-
return (
114-
(await props.beforeLeave({
115-
id: currentStep.id,
116-
index: currentStep.index,
117-
title: currentStep.title,
118-
})) !== false
119-
);
120-
} finally {
121-
isRunningBeforeLeave.value = false;
122-
}
123-
};
124-
125-
const changeTo = async (nextIndex: number, runGuard = true) => {
126-
if (nextIndex < 0 || nextIndex >= stepItems.value.length || nextIndex === activeIndex.value) {
127-
return false;
128-
}
129-
const currentIndex = activeIndex.value;
130-
if (runGuard) {
131-
const canLeave = await runBeforeLeave();
132-
if (activeIndex.value !== currentIndex) {
133-
return true;
134-
}
135-
if (!canLeave) {
136-
return false;
137-
}
138-
}
139-
activeIndex.value = nextIndex;
140-
emitChange();
141-
return true;
142-
};
143-
144-
const next = async () => {
145-
if (isRunningBeforeLeave.value) {
146-
const nextIndex = Math.min(activeIndex.value + 1, stepItems.value.length - 1);
147-
if (nextIndex !== activeIndex.value) {
148-
activeIndex.value = nextIndex;
149-
emitChange();
150-
}
151-
return true;
152-
}
153-
return changeTo(activeIndex.value + 1, true);
154-
};
155-
156-
const prev = async () => {
157-
return changeTo(activeIndex.value - 1, false);
158-
};
159-
160-
expose({
161-
next,
162-
prev,
163-
active: activeIndex,
164-
});
165-
166-
return () => {
167-
const currentStep = stepItems.value[activeIndex.value];
168-
return h('div', { class: ['fu-steps', `fu-steps--${props.direction}`] }, [
169-
h(
170-
'div',
171-
{
172-
class: 'fu-steps__nav',
173-
style: props.direction === 'vertical' && props.space ? { gap: `${props.space}px` } : undefined,
174-
},
175-
stepItems.value.map((step, index) =>
176-
h(
177-
'button',
178-
{
179-
key: step.id,
180-
class: [
181-
'fu-steps__item',
182-
{
183-
'is-active': index === activeIndex.value,
184-
'is-finished': index < activeIndex.value,
185-
},
186-
],
187-
type: 'button',
188-
disabled: props.isLoading,
189-
onClick: () => changeTo(step.index, step.index > activeIndex.value),
190-
},
191-
[
192-
h('span', { class: 'fu-steps__index' }, index + 1),
193-
h('span', { class: 'fu-steps__title' }, step.title),
194-
],
195-
),
196-
),
197-
),
198-
h('div', { class: 'fu-steps__content' }, currentStep ? buildStepChildren(currentStep.vnode) : []),
199-
slots.footer
200-
? h(
201-
'div',
202-
{ class: 'fu-steps__footer' },
203-
slots.footer({
204-
active: currentStep,
205-
next,
206-
prev,
207-
}),
208-
)
209-
: null,
210-
]);
211-
};
212-
},
213-
});
1+
export { default } from './steps/FuSteps';
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<template>
2+
<el-steps :active="active" v-bind="stepper">
3+
<el-step
4+
v-for="(step, index) in steps"
5+
:key="index"
6+
v-bind="step"
7+
:class="disable?.(index) && 'fu-step--disable'"
8+
@click="handleClick(index)"
9+
/>
10+
</el-steps>
11+
</template>
12+
13+
<script setup lang="ts">
14+
import { computed, type PropType } from 'vue';
15+
16+
import type { Step, Stepper } from './Stepper';
17+
18+
defineOptions({ name: 'FuHorizontalNavigation' });
19+
20+
const props = defineProps({
21+
stepper: Object as PropType<Stepper>,
22+
steps: Array as PropType<Step[]>,
23+
disable: Function as PropType<(index: number) => boolean>,
24+
});
25+
26+
const emit = defineEmits(['active']);
27+
28+
const active = computed(() => props.stepper?.index ?? 0);
29+
30+
const handleClick = (index: number) => {
31+
if (!props.disable?.(index)) {
32+
emit('active', index);
33+
}
34+
};
35+
</script>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { computed, defineComponent, h, provide, ref, Transition, watch } from 'vue';
2+
3+
import { flattenVNodes, getVNodeComponentName } from '../shared';
4+
import FuHorizontalNavigation from './FuHorizontalNavigation.vue';
5+
import FuStepsFooter from './FuStepsFooter';
6+
import { Step, Stepper } from './Stepper';
7+
8+
export default defineComponent({
9+
name: 'FuHorizontalSteps',
10+
emits: ['change', 'next', 'prev', 'onCancel', 'onFinish'],
11+
setup(_props, { attrs, slots, emit, expose }) {
12+
const stepper = ref(new Stepper());
13+
stepper.value.activeSet.add(0);
14+
15+
watch(
16+
() => stepper.value.index,
17+
(value) => {
18+
emit('change', stepper.value.steps[value]);
19+
},
20+
);
21+
22+
const heightStyle = computed(() => {
23+
const height = Number.parseInt(String(stepper.value.height ?? ''), 10);
24+
return Number.isFinite(height) ? { height: `${height}px` } : { height: 'auto' };
25+
});
26+
27+
const active = (index: number) => stepper.value.active(index);
28+
const disable = (index: number) => !stepper.value.isActive(index);
29+
const next = () => stepper.value.next();
30+
const prev = () => stepper.value.prev();
31+
const emitStepperFn = (name: 'next' | 'prev' | 'onCancel' | 'onFinish') => emit(name);
32+
33+
provide('stepper', stepper.value);
34+
35+
expose({ next, prev, active });
36+
37+
return () => {
38+
const stepNodes = flattenVNodes(slots.default?.() || []).filter(
39+
(node) => getVNodeComponentName(node) === 'FuStep',
40+
);
41+
const steps = stepNodes.map((node, index) => new Step({ index, ...((node.props || {}) as object) }));
42+
43+
Object.assign(stepper.value, attrs);
44+
stepper.value.steps = steps;
45+
46+
return h('div', { class: ['fu-steps', 'fu-steps--horizontal'] }, [
47+
h(FuHorizontalNavigation, {
48+
stepper: stepper.value,
49+
steps,
50+
disable,
51+
onActive: active,
52+
}),
53+
h('div', { class: 'fu-steps__wrapper' }, [
54+
h(
55+
'div',
56+
{ class: 'fu-steps__container', style: heightStyle.value },
57+
h(Transition, { name: 'carousel', mode: 'out-in' }, () =>
58+
stepNodes.map((node, index) =>
59+
stepper.value.index === index ? h(node, { key: index }) : null,
60+
),
61+
),
62+
),
63+
]),
64+
h(
65+
'div',
66+
{ class: 'fu-steps__footer' },
67+
slots.footer?.() || h(FuStepsFooter, { onStepperFn: emitStepperFn }),
68+
),
69+
]);
70+
};
71+
},
72+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<template>
2+
<div class="fu-step" v-loading="loading">
3+
<slot />
4+
</div>
5+
</template>
6+
7+
<script setup lang="ts">
8+
import { computed, inject } from 'vue';
9+
10+
import type { Stepper } from './Stepper';
11+
12+
defineOptions({ name: 'FuStep' });
13+
14+
defineProps({
15+
id: {
16+
type: String,
17+
default: '',
18+
},
19+
title: {
20+
type: String,
21+
default: '',
22+
},
23+
description: {
24+
type: String,
25+
default: '',
26+
},
27+
status: {
28+
type: String,
29+
default: '',
30+
},
31+
icon: {
32+
type: String,
33+
default: '',
34+
},
35+
});
36+
37+
const stepper = inject<Stepper>('stepper');
38+
39+
const loading = computed(() => {
40+
return stepper?.isLoading || false;
41+
});
42+
</script>

0 commit comments

Comments
 (0)