mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 16:00:51 +00:00
feat: add fu-step (#12562)
This commit is contained in:
@@ -1,18 +1 @@
|
||||
import { defineComponent, h } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FuStep',
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup(_props, { slots }) {
|
||||
return () => h('div', slots.default?.());
|
||||
},
|
||||
});
|
||||
export { default } from './steps/FuStep.vue';
|
||||
|
||||
@@ -1,213 +1 @@
|
||||
import { computed, defineComponent, h, ref, watch, type PropType, type VNode } from 'vue';
|
||||
|
||||
import { flattenVNodes, getVNodeComponentName } from './shared';
|
||||
|
||||
interface FuStepItem {
|
||||
id: string;
|
||||
index: number;
|
||||
title: string;
|
||||
vnode: VNode;
|
||||
}
|
||||
|
||||
const buildStepChildren = (vnode: VNode) => {
|
||||
const children = vnode.children as Record<string, (() => VNode[]) | undefined> | null;
|
||||
return children?.default?.() || [];
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FuSteps',
|
||||
props: {
|
||||
active: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: 'horizontal',
|
||||
},
|
||||
space: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
finishButtonText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
beforeLeave: {
|
||||
type: Function as PropType<
|
||||
(step: { id: string; index: number; title: string }) => boolean | Promise<boolean>
|
||||
>,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ['change'],
|
||||
setup(props, { emit, slots, expose }) {
|
||||
const activeIndex = ref(props.active);
|
||||
const isRunningBeforeLeave = ref(false);
|
||||
|
||||
const stepItems = computed<FuStepItem[]>(() =>
|
||||
flattenVNodes(slots.default?.() || [])
|
||||
.filter((vnode) => getVNodeComponentName(vnode) === 'FuStep')
|
||||
.map((vnode, index) => {
|
||||
const vnodeProps = (vnode.props || {}) as Record<string, any>;
|
||||
return {
|
||||
id: String(vnodeProps.id || index),
|
||||
index,
|
||||
title: String(vnodeProps.title || ''),
|
||||
vnode,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const emitChange = () => {
|
||||
const currentStep = stepItems.value[activeIndex.value];
|
||||
if (!currentStep) {
|
||||
return;
|
||||
}
|
||||
emit('change', {
|
||||
id: currentStep.id,
|
||||
index: currentStep.index,
|
||||
title: currentStep.title,
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.active,
|
||||
(value) => {
|
||||
activeIndex.value = value;
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
stepItems,
|
||||
(items) => {
|
||||
if (items.length === 0) {
|
||||
activeIndex.value = 0;
|
||||
return;
|
||||
}
|
||||
if (activeIndex.value > items.length - 1) {
|
||||
activeIndex.value = items.length - 1;
|
||||
}
|
||||
emitChange();
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
|
||||
const runBeforeLeave = async () => {
|
||||
if (!props.beforeLeave) {
|
||||
return true;
|
||||
}
|
||||
const currentStep = stepItems.value[activeIndex.value];
|
||||
if (!currentStep) {
|
||||
return true;
|
||||
}
|
||||
if (isRunningBeforeLeave.value) {
|
||||
return true;
|
||||
}
|
||||
isRunningBeforeLeave.value = true;
|
||||
try {
|
||||
return (
|
||||
(await props.beforeLeave({
|
||||
id: currentStep.id,
|
||||
index: currentStep.index,
|
||||
title: currentStep.title,
|
||||
})) !== false
|
||||
);
|
||||
} finally {
|
||||
isRunningBeforeLeave.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const changeTo = async (nextIndex: number, runGuard = true) => {
|
||||
if (nextIndex < 0 || nextIndex >= stepItems.value.length || nextIndex === activeIndex.value) {
|
||||
return false;
|
||||
}
|
||||
const currentIndex = activeIndex.value;
|
||||
if (runGuard) {
|
||||
const canLeave = await runBeforeLeave();
|
||||
if (activeIndex.value !== currentIndex) {
|
||||
return true;
|
||||
}
|
||||
if (!canLeave) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
activeIndex.value = nextIndex;
|
||||
emitChange();
|
||||
return true;
|
||||
};
|
||||
|
||||
const next = async () => {
|
||||
if (isRunningBeforeLeave.value) {
|
||||
const nextIndex = Math.min(activeIndex.value + 1, stepItems.value.length - 1);
|
||||
if (nextIndex !== activeIndex.value) {
|
||||
activeIndex.value = nextIndex;
|
||||
emitChange();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return changeTo(activeIndex.value + 1, true);
|
||||
};
|
||||
|
||||
const prev = async () => {
|
||||
return changeTo(activeIndex.value - 1, false);
|
||||
};
|
||||
|
||||
expose({
|
||||
next,
|
||||
prev,
|
||||
active: activeIndex,
|
||||
});
|
||||
|
||||
return () => {
|
||||
const currentStep = stepItems.value[activeIndex.value];
|
||||
return h('div', { class: ['fu-steps', `fu-steps--${props.direction}`] }, [
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'fu-steps__nav',
|
||||
style: props.direction === 'vertical' && props.space ? { gap: `${props.space}px` } : undefined,
|
||||
},
|
||||
stepItems.value.map((step, index) =>
|
||||
h(
|
||||
'button',
|
||||
{
|
||||
key: step.id,
|
||||
class: [
|
||||
'fu-steps__item',
|
||||
{
|
||||
'is-active': index === activeIndex.value,
|
||||
'is-finished': index < activeIndex.value,
|
||||
},
|
||||
],
|
||||
type: 'button',
|
||||
disabled: props.isLoading,
|
||||
onClick: () => changeTo(step.index, step.index > activeIndex.value),
|
||||
},
|
||||
[
|
||||
h('span', { class: 'fu-steps__index' }, index + 1),
|
||||
h('span', { class: 'fu-steps__title' }, step.title),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
h('div', { class: 'fu-steps__content' }, currentStep ? buildStepChildren(currentStep.vnode) : []),
|
||||
slots.footer
|
||||
? h(
|
||||
'div',
|
||||
{ class: 'fu-steps__footer' },
|
||||
slots.footer({
|
||||
active: currentStep,
|
||||
next,
|
||||
prev,
|
||||
}),
|
||||
)
|
||||
: null,
|
||||
]);
|
||||
};
|
||||
},
|
||||
});
|
||||
export { default } from './steps/FuSteps';
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<el-steps :active="active" v-bind="stepper">
|
||||
<el-step
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
v-bind="step"
|
||||
:class="disable?.(index) && 'fu-step--disable'"
|
||||
@click="handleClick(index)"
|
||||
/>
|
||||
</el-steps>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, type PropType } from 'vue';
|
||||
|
||||
import type { Step, Stepper } from './Stepper';
|
||||
|
||||
defineOptions({ name: 'FuHorizontalNavigation' });
|
||||
|
||||
const props = defineProps({
|
||||
stepper: Object as PropType<Stepper>,
|
||||
steps: Array as PropType<Step[]>,
|
||||
disable: Function as PropType<(index: number) => boolean>,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['active']);
|
||||
|
||||
const active = computed(() => props.stepper?.index ?? 0);
|
||||
|
||||
const handleClick = (index: number) => {
|
||||
if (!props.disable?.(index)) {
|
||||
emit('active', index);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,72 @@
|
||||
import { computed, defineComponent, h, provide, ref, Transition, watch } from 'vue';
|
||||
|
||||
import { flattenVNodes, getVNodeComponentName } from '../shared';
|
||||
import FuHorizontalNavigation from './FuHorizontalNavigation.vue';
|
||||
import FuStepsFooter from './FuStepsFooter';
|
||||
import { Step, Stepper } from './Stepper';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FuHorizontalSteps',
|
||||
emits: ['change', 'next', 'prev', 'onCancel', 'onFinish'],
|
||||
setup(_props, { attrs, slots, emit, expose }) {
|
||||
const stepper = ref(new Stepper());
|
||||
stepper.value.activeSet.add(0);
|
||||
|
||||
watch(
|
||||
() => stepper.value.index,
|
||||
(value) => {
|
||||
emit('change', stepper.value.steps[value]);
|
||||
},
|
||||
);
|
||||
|
||||
const heightStyle = computed(() => {
|
||||
const height = Number.parseInt(String(stepper.value.height ?? ''), 10);
|
||||
return Number.isFinite(height) ? { height: `${height}px` } : { height: 'auto' };
|
||||
});
|
||||
|
||||
const active = (index: number) => stepper.value.active(index);
|
||||
const disable = (index: number) => !stepper.value.isActive(index);
|
||||
const next = () => stepper.value.next();
|
||||
const prev = () => stepper.value.prev();
|
||||
const emitStepperFn = (name: 'next' | 'prev' | 'onCancel' | 'onFinish') => emit(name);
|
||||
|
||||
provide('stepper', stepper.value);
|
||||
|
||||
expose({ next, prev, active });
|
||||
|
||||
return () => {
|
||||
const stepNodes = flattenVNodes(slots.default?.() || []).filter(
|
||||
(node) => getVNodeComponentName(node) === 'FuStep',
|
||||
);
|
||||
const steps = stepNodes.map((node, index) => new Step({ index, ...((node.props || {}) as object) }));
|
||||
|
||||
Object.assign(stepper.value, attrs);
|
||||
stepper.value.steps = steps;
|
||||
|
||||
return h('div', { class: ['fu-steps', 'fu-steps--horizontal'] }, [
|
||||
h(FuHorizontalNavigation, {
|
||||
stepper: stepper.value,
|
||||
steps,
|
||||
disable,
|
||||
onActive: active,
|
||||
}),
|
||||
h('div', { class: 'fu-steps__wrapper' }, [
|
||||
h(
|
||||
'div',
|
||||
{ class: 'fu-steps__container', style: heightStyle.value },
|
||||
h(Transition, { name: 'carousel', mode: 'out-in' }, () =>
|
||||
stepNodes.map((node, index) =>
|
||||
stepper.value.index === index ? h(node, { key: index }) : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'fu-steps__footer' },
|
||||
slots.footer?.() || h(FuStepsFooter, { onStepperFn: emitStepperFn }),
|
||||
),
|
||||
]);
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="fu-step" v-loading="loading">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue';
|
||||
|
||||
import type { Stepper } from './Stepper';
|
||||
|
||||
defineOptions({ name: 'FuStep' });
|
||||
|
||||
defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const stepper = inject<Stepper>('stepper');
|
||||
|
||||
const loading = computed(() => {
|
||||
return stepper?.isLoading || false;
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<el-button :disabled="disabled" v-bind="$attrs">
|
||||
<slot />
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'FuStepButton' });
|
||||
|
||||
defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,42 @@
|
||||
import { defineComponent, h, ref } from 'vue';
|
||||
|
||||
import FuHorizontalSteps from './FuHorizontalSteps';
|
||||
import FuVerticalSteps from './FuVerticalSteps';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FuSteps',
|
||||
props: {
|
||||
direction: {
|
||||
type: String,
|
||||
default: 'horizontal',
|
||||
},
|
||||
},
|
||||
emits: ['change'],
|
||||
setup(props, { attrs, slots, emit, expose }) {
|
||||
const stepsRef = ref<{
|
||||
next?: () => unknown;
|
||||
prev?: () => unknown;
|
||||
active?: (index: number) => unknown;
|
||||
} | null>(null);
|
||||
|
||||
const next = () => stepsRef.value?.next();
|
||||
const prev = () => stepsRef.value?.prev();
|
||||
const active = (index: number) => stepsRef.value?.active(index);
|
||||
|
||||
const handleChange = (payload: any) => {
|
||||
emit('change', payload);
|
||||
};
|
||||
|
||||
expose({
|
||||
next,
|
||||
prev,
|
||||
active,
|
||||
});
|
||||
|
||||
if (props.direction === 'vertical') {
|
||||
return () => h(FuVerticalSteps, { ref: stepsRef, onChange: handleChange, ...attrs }, slots);
|
||||
}
|
||||
|
||||
return () => h(FuHorizontalSteps, { ref: stepsRef, onChange: handleChange, ...attrs }, slots);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { computed, defineComponent, h, inject, ref } from 'vue';
|
||||
|
||||
import FuStepButton from './FuStepButton.vue';
|
||||
import type { Stepper } from './Stepper';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FuStepsFooter',
|
||||
emits: ['stepperFn'],
|
||||
setup(_props, { emit }) {
|
||||
const stepper = inject<Stepper>('stepper');
|
||||
const disabledButton = ref(false);
|
||||
|
||||
const isFirst = computed(() => {
|
||||
return stepper?.isFirst(stepper.index) ?? true;
|
||||
});
|
||||
|
||||
const isLast = computed(() => {
|
||||
return stepper?.isLast(stepper.index) ?? true;
|
||||
});
|
||||
|
||||
const showCancel = computed(() => {
|
||||
return stepper?.showCancel !== false;
|
||||
});
|
||||
|
||||
const disabled = computed(() => {
|
||||
return Boolean(stepper?.isLoading || disabledButton.value);
|
||||
});
|
||||
|
||||
const clickHandle = (fnName: string) => {
|
||||
if (!stepper) {
|
||||
return;
|
||||
}
|
||||
const fn = (stepper as any)[fnName];
|
||||
if (typeof fn === 'function') {
|
||||
fn.call(stepper);
|
||||
} else {
|
||||
emit('stepperFn', fnName);
|
||||
}
|
||||
disabledButton.value = true;
|
||||
setTimeout(() => {
|
||||
disabledButton.value = false;
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const renderButton = (value: string) => {
|
||||
if (!stepper) {
|
||||
return null;
|
||||
}
|
||||
return h(
|
||||
FuStepButton,
|
||||
{
|
||||
disabled: disabled.value,
|
||||
size: stepper.buttonSize,
|
||||
onClick: () => clickHandle(value),
|
||||
},
|
||||
() => (stepper as any)[`${value}ButtonText`],
|
||||
);
|
||||
};
|
||||
|
||||
return () =>
|
||||
h('div', { class: `fu-steps__footer--${stepper?.footerAlign ?? 'flex'}` }, [
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'fu-steps__footer--block',
|
||||
style: 'margin-right: 10px',
|
||||
},
|
||||
[showCancel.value ? renderButton('onCancel') : null],
|
||||
),
|
||||
h('div', { class: 'fu-steps__footer--block' }, [
|
||||
!isFirst.value ? renderButton('prev') : null,
|
||||
isLast.value ? renderButton('onFinish') : renderButton('next'),
|
||||
]),
|
||||
]);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<el-steps :active="active" v-bind="stepper" direction="vertical">
|
||||
<el-step
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
v-bind="step"
|
||||
:class="disable?.(index) && 'fu-step--disable'"
|
||||
@click="handleClick(index)"
|
||||
>
|
||||
<template #description>
|
||||
<span>{{ step.description }}</span>
|
||||
<el-collapse-transition>
|
||||
<div v-if="index === active" class="fu-steps__container" :style="heightStyle">
|
||||
<slot :step="step" />
|
||||
</div>
|
||||
</el-collapse-transition>
|
||||
</template>
|
||||
</el-step>
|
||||
</el-steps>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, type PropType } from 'vue';
|
||||
|
||||
import type { Step, Stepper } from './Stepper';
|
||||
|
||||
defineOptions({ name: 'FuVerticalNavigation' });
|
||||
|
||||
const props = defineProps({
|
||||
stepper: Object as PropType<Stepper>,
|
||||
steps: Array as PropType<Step[]>,
|
||||
disable: Function as PropType<(index: number) => boolean>,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['active']);
|
||||
|
||||
const active = computed(() => props.stepper?.index ?? 0);
|
||||
|
||||
const heightStyle = computed(() => {
|
||||
const height = Number.parseInt(String(props.stepper?.height ?? ''), 10);
|
||||
return Number.isFinite(height) ? { height: `${height}px` } : undefined;
|
||||
});
|
||||
|
||||
const handleClick = (index: number) => {
|
||||
if (!props.disable?.(index)) {
|
||||
emit('active', index);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,61 @@
|
||||
import { defineComponent, h, provide, ref, watch } from 'vue';
|
||||
|
||||
import { flattenVNodes, getVNodeComponentName } from '../shared';
|
||||
import FuStepsFooter from './FuStepsFooter';
|
||||
import FuVerticalNavigation from './FuVerticalNavigation.vue';
|
||||
import { Step, Stepper } from './Stepper';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FuVerticalSteps',
|
||||
emits: ['change', 'next', 'prev', 'onCancel', 'onFinish'],
|
||||
setup(_props, { attrs, slots, emit, expose }) {
|
||||
const stepper = ref(new Stepper());
|
||||
stepper.value.activeSet.add(0);
|
||||
|
||||
watch(
|
||||
() => stepper.value.index,
|
||||
(value) => {
|
||||
emit('change', stepper.value.steps[value]);
|
||||
},
|
||||
);
|
||||
|
||||
const active = (index: number) => stepper.value.active(index);
|
||||
const disable = (index: number) => !stepper.value.isActive(index);
|
||||
const next = () => stepper.value.next();
|
||||
const prev = () => stepper.value.prev();
|
||||
const emitStepperFn = (name: 'next' | 'prev' | 'onCancel' | 'onFinish') => emit(name);
|
||||
|
||||
provide('stepper', stepper.value);
|
||||
|
||||
expose({ next, prev, active });
|
||||
|
||||
return () => {
|
||||
const stepNodes = flattenVNodes(slots.default?.() || []).filter(
|
||||
(node) => getVNodeComponentName(node) === 'FuStep',
|
||||
);
|
||||
const steps = stepNodes.map((node, index) => new Step({ index, ...((node.props || {}) as object) }));
|
||||
const currentNode = stepNodes.find((_node, index) => stepper.value.isCurrent(index));
|
||||
|
||||
Object.assign(stepper.value, attrs);
|
||||
stepper.value.steps = steps;
|
||||
|
||||
return h('div', { class: ['fu-steps', 'fu-steps--vertical'] }, [
|
||||
h(
|
||||
FuVerticalNavigation,
|
||||
{
|
||||
stepper: stepper.value,
|
||||
steps,
|
||||
disable,
|
||||
onActive: active,
|
||||
},
|
||||
() => currentNode,
|
||||
),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'fu-steps__footer' },
|
||||
slots.footer?.() || h(FuStepsFooter, { onStepperFn: emitStepperFn }),
|
||||
),
|
||||
]);
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import i18n from '@/lang';
|
||||
|
||||
import type { StepOptions, StepperOptions } from './types';
|
||||
|
||||
export class Step implements StepOptions {
|
||||
id?: string;
|
||||
index: number;
|
||||
beforeActive?: Function;
|
||||
beforeLeave?: Function;
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
status?: string;
|
||||
|
||||
constructor(options?: StepOptions) {
|
||||
const stepOptions = options ?? ({ index: 0 } as StepOptions);
|
||||
this.id = stepOptions.id;
|
||||
this.index = stepOptions.index;
|
||||
this.beforeActive = stepOptions.beforeActive;
|
||||
this.beforeLeave = stepOptions.beforeLeave;
|
||||
this.title = stepOptions.title;
|
||||
this.description = stepOptions.description;
|
||||
this.icon = stepOptions.icon;
|
||||
this.status = stepOptions.status;
|
||||
}
|
||||
}
|
||||
|
||||
export class Stepper implements StepperOptions {
|
||||
steps: Step[];
|
||||
index: number;
|
||||
activeSet: Set<number>;
|
||||
isLoading: boolean;
|
||||
onCancelButtonText: string;
|
||||
onFinishButtonText: string;
|
||||
prevButtonText: string;
|
||||
nextButtonText: string;
|
||||
buttonSize: string;
|
||||
footerAlign: string;
|
||||
showCancel: boolean;
|
||||
beforeActive?: Function;
|
||||
beforeLeave?: Function;
|
||||
height?: string | number;
|
||||
|
||||
constructor(options?: Partial<StepperOptions>) {
|
||||
this.steps = options?.steps?.map((step) => new Step(step)) || [];
|
||||
this.index = options?.index ?? 0;
|
||||
this.activeSet = new Set<number>();
|
||||
this.isLoading = options?.isLoading ?? false;
|
||||
this.onCancelButtonText = options?.onCancelButtonText ?? i18n.global.t('fu.steps.cancel');
|
||||
this.onFinishButtonText = options?.onFinishButtonText ?? i18n.global.t('fu.steps.finish');
|
||||
this.prevButtonText = options?.prevButtonText ?? i18n.global.t('fu.steps.prev');
|
||||
this.nextButtonText = options?.nextButtonText ?? i18n.global.t('fu.steps.next');
|
||||
this.buttonSize = options?.buttonSize ?? 'default';
|
||||
this.footerAlign = options?.footerAlign ?? 'flex';
|
||||
this.showCancel = options?.showCancel ?? false;
|
||||
this.beforeActive = options?.beforeActive;
|
||||
this.beforeLeave = options?.beforeLeave;
|
||||
this.height = options?.height;
|
||||
}
|
||||
|
||||
isFirst(index: number) {
|
||||
return index === 0;
|
||||
}
|
||||
|
||||
isLast(index: number) {
|
||||
return index === this.steps.length - 1;
|
||||
}
|
||||
|
||||
isActive(index: number) {
|
||||
return this.activeSet.has(index);
|
||||
}
|
||||
|
||||
isCurrent(index: number) {
|
||||
return this.index === index;
|
||||
}
|
||||
|
||||
async active(index: number) {
|
||||
const isValid = index >= 0 && index < this.steps.length && this.index !== index;
|
||||
const forward = index > this.index;
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
if ((await this.executeBeforeLeave(this.index, forward)) === false) {
|
||||
return;
|
||||
}
|
||||
if ((await this.executeBeforeActive(index, forward)) === false) {
|
||||
return;
|
||||
}
|
||||
this.index = index;
|
||||
this.activeSet.add(index);
|
||||
}
|
||||
|
||||
next() {
|
||||
if (!this.isLast(this.index)) {
|
||||
return this.active(this.index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
prev() {
|
||||
if (!this.isFirst(this.index)) {
|
||||
return this.active(this.index - 1);
|
||||
}
|
||||
}
|
||||
|
||||
getStep(index: number) {
|
||||
return this.steps[index];
|
||||
}
|
||||
|
||||
executeBeforeLeave(index: number, forward: boolean) {
|
||||
const step = this.getStep(index);
|
||||
if (step?.beforeLeave) {
|
||||
return step.beforeLeave(step, forward);
|
||||
}
|
||||
return this.beforeLeave?.(step, forward);
|
||||
}
|
||||
|
||||
executeBeforeActive(index: number, forward: boolean) {
|
||||
const step = this.getStep(index);
|
||||
if (step?.beforeActive) {
|
||||
return step.beforeActive(step, forward);
|
||||
}
|
||||
return this.beforeActive?.(step, forward);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
export interface StepOptions {
|
||||
id?: string;
|
||||
index: number;
|
||||
beforeActive?: Function;
|
||||
beforeLeave?: Function;
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface StepperOptions {
|
||||
steps: StepOptions[];
|
||||
index: number;
|
||||
activeSet: Set<number>;
|
||||
isLoading?: boolean;
|
||||
onCancelButtonText: string;
|
||||
onFinishButtonText: string;
|
||||
prevButtonText: string;
|
||||
nextButtonText: string;
|
||||
buttonSize: string;
|
||||
footerAlign: string;
|
||||
showCancel: boolean;
|
||||
beforeActive?: Function;
|
||||
beforeLeave?: Function;
|
||||
height?: string | number;
|
||||
}
|
||||
+79
-51
@@ -107,71 +107,99 @@
|
||||
}
|
||||
|
||||
.fu-steps {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
.fu-steps__footer {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.fu-steps__footer--block {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.fu-steps__footer--flex {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.fu-steps__footer--left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.fu-steps__footer--right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.fu-steps__footer--center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fu-steps__container {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.fu-steps--horizontal {
|
||||
.fu-steps__wrapper {
|
||||
overflow: hidden;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.fu-steps__container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.fu-step {
|
||||
width: 100%;
|
||||
flex: 1 0 100%;
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.fu-steps--vertical {
|
||||
align-items: flex-start;
|
||||
.el-step__description {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.fu-step {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.fu-steps__nav {
|
||||
display: flex;
|
||||
.fu-step--disable {
|
||||
cursor: not-allowed !important;
|
||||
|
||||
.is-wait {
|
||||
color: #c0c4cc !important;
|
||||
border-color: #c0c4cc !important;
|
||||
}
|
||||
}
|
||||
|
||||
.fu-steps--vertical .fu-steps__nav {
|
||||
flex-direction: column;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.fu-steps__item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
color: var(--el-text-color-regular);
|
||||
.el-step:hover {
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
|
||||
.is-wait {
|
||||
color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.fu-steps__item.is-active {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 600;
|
||||
.carousel-enter-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.fu-steps__item.is-finished .fu-steps__index {
|
||||
background: var(--el-color-success);
|
||||
color: var(--el-color-white);
|
||||
.carousel-leave-active {
|
||||
transition: all 0.5s cubic-bezier(1, 0.5, 0.8, 1);
|
||||
}
|
||||
|
||||
.fu-steps__item:disabled {
|
||||
cursor: not-allowed;
|
||||
.carousel-enter-from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fu-steps__index {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fu-steps__title {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.fu-steps__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.fu-steps__footer {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
.carousel-leave-to {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user