fix(marketplace): онбординг ЦПП — единый список 1-2-3-4 + лоадер внутри карточки
Шаги 3–4 (участки, ПВЗ) больше не вынесены во вторую карточку «Дальнейшая настройка» — они идут тем же сквозным списком после шагов совета (extraSteps в CouncilOnboardingCard), нумерация продолжается. Доп.шаги видны всегда, в том числе после утверждения документов. Лоадер вынесен из оверлея на всё окно (WindowLoader, .absolute-full) внутрь карточки (q-inner-loading с резервом высоты) — больше не «болтается» поверх уже отрисованной страницы. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+38
-113
@@ -1,8 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { CouncilOnboardingCard } from 'src/shared/ui/CouncilOnboarding';
|
||||
import { BaseBadge, BaseButton } from 'src/shared/ui/base';
|
||||
import {
|
||||
CouncilOnboardingCard,
|
||||
type ICouncilOnboardingExtraStep,
|
||||
} from 'src/shared/ui/CouncilOnboarding';
|
||||
import { BaseBadge } from 'src/shared/ui/base';
|
||||
import type { BaseBadgeVariant } from 'src/shared/ui/base';
|
||||
import { useSystemStore } from 'src/entities/System/model';
|
||||
import { useMarketplaceOnboarding } from '../model/composable';
|
||||
@@ -16,6 +19,10 @@ import { useMarketplaceOnboarding } from '../model/composable';
|
||||
* решения; статус шага меняется на «завершён» по РЕАЛЬНОМУ ончейн-решению
|
||||
* совета (без stub-кнопки). Когда оба документа утверждены — расширение
|
||||
* подключается автоматически и пайщики получают доступ к Столу заказов.
|
||||
*
|
||||
* Шаги 3–4 (добавить кооперативные участки, назначить ПВЗ) выполняются на
|
||||
* других столах — здесь они идут тем же сквозным списком (extraSteps) со
|
||||
* ссылками, без дублирования функционала.
|
||||
*/
|
||||
|
||||
const { config, loading, submitting, isCompleted, loadState, handleStepSubmit } =
|
||||
@@ -28,35 +35,33 @@ const chipLabel = computed(() =>
|
||||
isCompleted.value ? 'Подключено' : 'Не подключено',
|
||||
);
|
||||
|
||||
// Шаги 3–4 после утверждения двух документов Советом: они выполняются на других
|
||||
// столах, поэтому здесь — только подсказка со ссылками (без дублирования
|
||||
// функционала добавления участков/ПВЗ).
|
||||
const router = useRouter();
|
||||
const systemStore = useSystemStore();
|
||||
const coopname = computed(() => systemStore.info?.coopname || '');
|
||||
|
||||
const NEXT_STEPS = [
|
||||
// id доп.шага = имя маршрута стола, куда ведём.
|
||||
const EXTRA_STEPS = computed<ICouncilOnboardingExtraStep[]>(() => [
|
||||
{
|
||||
n: 3,
|
||||
id: 'branches',
|
||||
title: 'Добавьте кооперативные участки',
|
||||
description:
|
||||
'Кооперативные участки создаются юридически вне системы, а здесь добавляются уже оформленные участки с их председателями. Без хотя бы одного участка пунктам выдачи не на чем работать.',
|
||||
cta: 'Перейти к участкам',
|
||||
route: 'branches',
|
||||
actionLabel: 'Перейти к участкам',
|
||||
disabled: !coopname.value,
|
||||
},
|
||||
{
|
||||
n: 4,
|
||||
id: 'marketplace-issuance-points',
|
||||
title: 'Назначьте пункты выдачи (ПВЗ)',
|
||||
description:
|
||||
'Отметьте нужные кооперативные участки как пункты выдачи заказов и задайте режим их работы — тогда пайщики смогут выбирать ПВЗ при заказе.',
|
||||
cta: 'Перейти к ПВЗ',
|
||||
route: 'marketplace-issuance-points',
|
||||
actionLabel: 'Перейти к ПВЗ',
|
||||
disabled: !coopname.value,
|
||||
},
|
||||
] as const;
|
||||
]);
|
||||
|
||||
function goTo(name: string): void {
|
||||
function onExtraAction(step: ICouncilOnboardingExtraStep): void {
|
||||
if (!coopname.value) return;
|
||||
void router.push({ name, params: { coopname: coopname.value } });
|
||||
void router.push({ name: step.id, params: { coopname: coopname.value } });
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -66,110 +71,30 @@ onMounted(async () => {
|
||||
|
||||
<template lang="pug">
|
||||
q-page.onboarding-l1(role="region", aria-label="Подключение ЦПП Стол заказов")
|
||||
.onboarding-l1__card
|
||||
CouncilOnboardingCard(
|
||||
:config="config",
|
||||
:loading="loading",
|
||||
:submitting="submitting",
|
||||
title="Подключение ЦПП «Стол заказов»",
|
||||
subtitle="Целевая Потребительская Программа должна быть принята Советом кооператива, прежде чем пайщики смогут пользоваться Столом заказов.",
|
||||
:completion-title="config.completionTitle",
|
||||
:completion-message="config.completionMessage",
|
||||
@step-submit="handleStepSubmit"
|
||||
)
|
||||
template(#status)
|
||||
BaseBadge(:variant="chipVariant", dot) {{ chipLabel }}
|
||||
|
||||
.onboarding-l1__next
|
||||
.onboarding-l1__next-head Дальнейшая настройка
|
||||
.onboarding-l1__next-sub Эти шаги выполняются на других столах — после них Стол заказов готов к работе.
|
||||
.onboarding-l1__step(v-for="step in NEXT_STEPS", :key="step.n")
|
||||
.onboarding-l1__step-num {{ step.n }}
|
||||
.onboarding-l1__step-body
|
||||
.onboarding-l1__step-title {{ step.title }}
|
||||
.onboarding-l1__step-desc {{ step.description }}
|
||||
BaseButton.onboarding-l1__step-cta(
|
||||
variant="ghost",
|
||||
size="sm",
|
||||
:disabled="!coopname",
|
||||
@click="goTo(step.route)"
|
||||
)
|
||||
| {{ step.cta }}
|
||||
template(#icon-right)
|
||||
q-icon(name="arrow_forward", size="16px")
|
||||
CouncilOnboardingCard(
|
||||
:config="config",
|
||||
:loading="loading",
|
||||
:submitting="submitting",
|
||||
:extra-steps="EXTRA_STEPS",
|
||||
title="Подключение ЦПП «Стол заказов»",
|
||||
subtitle="Целевая Потребительская Программа должна быть принята Советом кооператива, прежде чем пайщики смогут пользоваться Столом заказов.",
|
||||
:completion-title="config.completionTitle",
|
||||
:completion-message="config.completionMessage",
|
||||
@step-submit="handleStepSubmit",
|
||||
@extra-action="onExtraAction"
|
||||
)
|
||||
template(#status)
|
||||
BaseBadge(:variant="chipVariant", dot) {{ chipLabel }}
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.onboarding-l1 {
|
||||
padding: var(--p-6, 24px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-4, 16px);
|
||||
}
|
||||
|
||||
&__card {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__next {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-3, 12px);
|
||||
padding: var(--p-5, 20px);
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-lg, 12px);
|
||||
}
|
||||
|
||||
&__next-head {
|
||||
font-size: var(--p-fs-h3);
|
||||
font-weight: 600;
|
||||
color: var(--p-ink-1);
|
||||
}
|
||||
|
||||
&__next-sub {
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
color: var(--p-ink-3);
|
||||
}
|
||||
|
||||
&__step {
|
||||
display: flex;
|
||||
gap: var(--p-3, 12px);
|
||||
align-items: flex-start;
|
||||
padding-top: var(--p-3, 12px);
|
||||
border-top: 1px solid var(--p-line);
|
||||
}
|
||||
|
||||
&__step-num {
|
||||
flex: 0 0 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--p-surface-2);
|
||||
color: var(--p-ink-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__step-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-2, 8px);
|
||||
}
|
||||
|
||||
&__step-title {
|
||||
font-weight: 600;
|
||||
color: var(--p-ink-1);
|
||||
}
|
||||
|
||||
&__step-desc {
|
||||
font-size: var(--p-fs-body-sm, 13px);
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
|
||||
&__step-cta {
|
||||
align-self: flex-start;
|
||||
@media (max-width: 768px) {
|
||||
.onboarding-l1 {
|
||||
padding: var(--p-4, 16px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,71 +1,81 @@
|
||||
<template lang="pug">
|
||||
// Показываем лоадер пока данные загружаются
|
||||
WindowLoader(
|
||||
v-if="loading"
|
||||
:text="loadingText"
|
||||
)
|
||||
//- Единая карточка онбординга. Лоадер — внутри карточки (q-inner-loading),
|
||||
//- а не оверлеем на всё окно: иначе он «болтается» поверх уже отрисованной
|
||||
//- страницы. Шаги совета и доп.шаги (навигация на другие столы) идут одним
|
||||
//- сквозным списком 1-2-3-4 — без второй карточки и заголовков-разделов.
|
||||
q-card.council-onboarding(flat, :class="{ 'council-onboarding--loading': loading }")
|
||||
q-inner-loading(:showing="loading")
|
||||
q-spinner(color="primary", size="2.5em")
|
||||
div.text-caption.text-grey-7.q-mt-sm(v-if="loadingText") {{ loadingText }}
|
||||
|
||||
// Показываем поздравление если онбординг завершен — канон-состояние
|
||||
// EmptyState (центрированная плитка-иконка + заголовок + тело), с
|
||||
// success-акцентом на иконке вместо «голой» крупной иконки в карточке.
|
||||
slot(name="completion" v-else-if="isCompleted")
|
||||
q-card(flat)
|
||||
EmptyState.council-onboarding__done(
|
||||
:title="completionTitle"
|
||||
:body="completionMessage"
|
||||
)
|
||||
template(#icon)
|
||||
q-icon(name="celebration" size="26px")
|
||||
template(v-if="!loading")
|
||||
//- Поздравление, если шаги совета завершены — канон-состояние EmptyState.
|
||||
slot(name="completion", v-if="isCompleted")
|
||||
EmptyState.council-onboarding__done(
|
||||
:title="completionTitle",
|
||||
:body="completionMessage"
|
||||
)
|
||||
template(#icon)
|
||||
q-icon(name="celebration", size="26px")
|
||||
|
||||
// Показываем шаги если онбординг не завершен и данные загружены
|
||||
q-card(v-else flat)
|
||||
q-card-section
|
||||
div.text-h5 {{ title }}
|
||||
div.text-caption.text-grey-7.q-mt-xs(v-if="subtitle") {{ subtitle }}
|
||||
// Таймер и статус подключения в одном ряду — статус (напр. чип
|
||||
// «Не подключено») переехал сюда из отдельной шапки страницы, чтобы
|
||||
// не дублировать заголовок. Свой flex с gap, а не Quasar .row: у .row
|
||||
// включён flex-wrap и при justify-between одиночный чип сваливался бы
|
||||
// в начало новой строки.
|
||||
div.council-onboarding__status-row(v-if="countdownLabel || hasStatusSlot")
|
||||
q-chip(
|
||||
v-if="countdownLabel"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
icon="schedule"
|
||||
) {{ countdownLabel }}
|
||||
slot(name="status")
|
||||
q-separator
|
||||
q-card-section
|
||||
q-list(separator)
|
||||
q-item.council-onboarding__step(v-for="(step, index) in steps" :key="step.id")
|
||||
q-item-section
|
||||
div.row.items-center.q-gutter-sm
|
||||
q-icon(
|
||||
:name="getIcon(step)"
|
||||
:color="getIconColor(step)"
|
||||
size="22px"
|
||||
)
|
||||
div.text-subtitle1 {{ index + 1 }}. {{ step.title }}
|
||||
div.text-caption.text-grey-7.q-mt-xs {{ step.description }}
|
||||
// Чип-статус в блочной обёртке: q-item-section — flex-column со
|
||||
// stretch, прямой q-chip растянулся бы на всю ширину. Обёртка
|
||||
// тянется, а inline-flex чип внутри хагает контент слева.
|
||||
div.q-mt-sm(v-if="step.status === 'in_progress'")
|
||||
q-chip.q-ma-none(
|
||||
dense
|
||||
color="amber"
|
||||
text-color="black"
|
||||
icon="hourglass_top"
|
||||
) Ожидаем решение совета
|
||||
q-item-section(side top)
|
||||
q-btn(
|
||||
v-if="showAction(index)"
|
||||
:disable="submitting"
|
||||
color="primary"
|
||||
label="Объявить собрание совета"
|
||||
@click="() => handleStepClick(step)"
|
||||
)
|
||||
//- Иначе — шапка и шаги совета.
|
||||
template(v-else)
|
||||
q-card-section
|
||||
div.text-h5 {{ title }}
|
||||
div.text-caption.text-grey-7.q-mt-xs(v-if="subtitle") {{ subtitle }}
|
||||
div.council-onboarding__status-row(v-if="countdownLabel || hasStatusSlot")
|
||||
q-chip(
|
||||
v-if="countdownLabel",
|
||||
color="primary",
|
||||
text-color="white",
|
||||
icon="schedule"
|
||||
) {{ countdownLabel }}
|
||||
slot(name="status")
|
||||
q-separator
|
||||
q-card-section
|
||||
q-list(separator)
|
||||
q-item.council-onboarding__step(v-for="(step, index) in steps", :key="step.id")
|
||||
q-item-section
|
||||
div.row.items-center.q-gutter-sm
|
||||
q-icon(:name="getIcon(step)", :color="getIconColor(step)", size="22px")
|
||||
div.text-subtitle1 {{ index + 1 }}. {{ step.title }}
|
||||
div.text-caption.text-grey-7.q-mt-xs {{ step.description }}
|
||||
div.q-mt-sm(v-if="step.status === 'in_progress'")
|
||||
q-chip.q-ma-none(
|
||||
dense,
|
||||
color="amber",
|
||||
text-color="black",
|
||||
icon="hourglass_top"
|
||||
) Ожидаем решение совета
|
||||
q-item-section(side, top)
|
||||
q-btn(
|
||||
v-if="showAction(index)",
|
||||
:disable="submitting",
|
||||
color="primary",
|
||||
label="Объявить собрание совета",
|
||||
@click="() => handleStepClick(step)"
|
||||
)
|
||||
|
||||
//- Доп.шаги — показываем ВСЕГДА (в т.ч. после завершения шагов совета),
|
||||
//- нумерация продолжает шаги совета. Единый список, без отдельной карточки.
|
||||
template(v-if="extraStepsList.length")
|
||||
q-separator
|
||||
q-card-section
|
||||
q-list(separator)
|
||||
q-item.council-onboarding__step(v-for="(step, i) in extraStepsList", :key="step.id")
|
||||
q-item-section
|
||||
div.row.items-center.q-gutter-sm
|
||||
q-icon(name="radio_button_unchecked", color="grey-6", size="22px")
|
||||
div.text-subtitle1 {{ steps.length + i + 1 }}. {{ step.title }}
|
||||
div.text-caption.text-grey-7.q-mt-xs {{ step.description }}
|
||||
q-item-section(side, top)
|
||||
q-btn(
|
||||
outline,
|
||||
color="primary",
|
||||
:label="step.actionLabel",
|
||||
:disable="step.disabled",
|
||||
@click="() => emit('extra-action', step)"
|
||||
)
|
||||
|
||||
BaseDialog(
|
||||
v-model='dialogOpen',
|
||||
@@ -100,8 +110,11 @@ import { DocumentHtmlReader } from 'src/shared/ui/DocumentHtmlReader';
|
||||
import { EmptyState } from 'src/shared/ui/base/EmptyState';
|
||||
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
|
||||
import { BaseButton } from 'src/shared/ui/base/BaseButton';
|
||||
import { WindowLoader } from 'src/shared/ui/Loader';
|
||||
import type { ICouncilOnboardingStep, ICouncilOnboardingConfig } from './types';
|
||||
import type {
|
||||
ICouncilOnboardingStep,
|
||||
ICouncilOnboardingConfig,
|
||||
ICouncilOnboardingExtraStep,
|
||||
} from './types';
|
||||
|
||||
interface Props {
|
||||
config: ICouncilOnboardingConfig;
|
||||
@@ -114,6 +127,10 @@ interface Props {
|
||||
subtitle?: string;
|
||||
completionTitle?: string;
|
||||
completionMessage?: string;
|
||||
// Доп.шаги (навигация на другие столы) — рисуются в общем списке после
|
||||
// шагов совета и видны всегда. По умолчанию пусто (другие онбординги не
|
||||
// передают — поведение не меняется).
|
||||
extraSteps?: ICouncilOnboardingExtraStep[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -123,10 +140,12 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
title: 'Адаптация к работе на платформе',
|
||||
completionTitle: 'Онбординг завершен!',
|
||||
completionMessage: 'Все необходимые документы утверждены.',
|
||||
extraSteps: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'step-submit', step: ICouncilOnboardingStep): void;
|
||||
(e: 'extra-action', step: ICouncilOnboardingExtraStep): void;
|
||||
}>();
|
||||
|
||||
const slots = useSlots();
|
||||
@@ -142,6 +161,8 @@ const dialogDecision = ref('');
|
||||
const dialogDecisionPrefix = ref('');
|
||||
|
||||
const steps = computed(() => props.config.steps);
|
||||
// Null-safe доступ к доп.шагам: prop опционален, дефолт — пустой список.
|
||||
const extraStepsList = computed(() => props.extraSteps ?? []);
|
||||
|
||||
const countdownLabel = computed(() => {
|
||||
if (!props.config.expireAt) return null;
|
||||
@@ -228,6 +249,12 @@ watch(
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// Пока идёт загрузка — резервируем высоту, чтобы q-inner-loading центрировал
|
||||
// спиннер в осмысленной области, а не в схлопнутой пустой карточке.
|
||||
.council-onboarding--loading {
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
// Таймер + статус подключения в один ряд с предсказуемым зазором. Не Quasar
|
||||
// .row + .q-gutter: у чипов свои дефолтные margin'ы, из-за которых зазор
|
||||
// «прыгает»; здесь gap + сброс margin дают ровную линию и аккуратный перенос.
|
||||
|
||||
@@ -16,6 +16,19 @@ export interface ICouncilOnboardingStep {
|
||||
depends_on?: string[]; // ID шагов, которые должны быть завершены перед этим шагом
|
||||
}
|
||||
|
||||
/**
|
||||
* Доп. шаг онбординга, не связанный с собранием совета: навигация на другой
|
||||
* стол (например «добавить кооперативные участки», «назначить ПВЗ»). Рисуется в
|
||||
* том же списке после шагов совета, продолжая сквозную нумерацию.
|
||||
*/
|
||||
export interface ICouncilOnboardingExtraStep {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
actionLabel: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ICouncilOnboardingConfig {
|
||||
steps: ICouncilOnboardingStep[];
|
||||
expireAt?: Date | null;
|
||||
|
||||
Reference in New Issue
Block a user