|
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'; |
0 commit comments