feat(autopodbor): показ очереди и этапа клиенту во время сбора (фронт)
Компонент RunCollectStatus: «вы в очереди №N» (пока прогон ждёт) или «этап N из M: подпись» (пока идёт). Встроен в оба окна сбора (конкуренты/источники) и экран загрузки ручного изучения. RunDto получил queue_position и progress. Никаких фейковых процентов — только реальные этапы движка. 4 теста Vitest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,17 @@ export interface RunDto {
|
||||
finished_at: string | null;
|
||||
created_at: string | null;
|
||||
competitor_id: number | null;
|
||||
/** Место в общей очереди (пока прогон ждёт запуска). null — если уже идёт/завершён. */
|
||||
queue_position: number | null;
|
||||
/** Текущий этап работы (пока прогон идёт): «этап stage из total: label». null — если ждёт/завершён. */
|
||||
progress: RunProgress | null;
|
||||
}
|
||||
|
||||
/** Этап живого прогона для показа клиенту. */
|
||||
export interface RunProgress {
|
||||
stage: number;
|
||||
total: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export type Box = 'proposal' | 'field' | 'archived';
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAutopodborStore } from '../../../stores/autopodborStore';
|
||||
import { autopodborErrorMessage, type FieldSourceDto, type WhereFound } from '../../../api/autopodbor';
|
||||
import { REGIONS, FEDERAL_DISTRICT_NAMES } from '../../../constants/regions';
|
||||
import { useOverlayDismiss } from '../../../composables/useOverlayDismiss';
|
||||
import RunCollectStatus from './RunCollectStatus.vue';
|
||||
|
||||
interface AutopodborNav {
|
||||
go: (s: string) => void;
|
||||
@@ -302,17 +303,17 @@ async function saveAdd() {
|
||||
|
||||
// ——— создать проект из источника ———
|
||||
// regions=[] + vsyaRf=true → «Вся РФ» (как в обычных проектах: пустой массив = вся страна).
|
||||
const create = reactive({ open: false, srcIds: [] as number[], srcLabel: '', regions: [] as number[], vsyaRf: true, limit: 5, mask: 127 });
|
||||
const create = reactive({ open: false, srcIds: [] as number[], srcLabel: '', regions: [] as number[], vsyaRf: false, limit: 5, mask: 127 });
|
||||
function openCreate(s: FieldSourceDto) {
|
||||
regQuery.value = '';
|
||||
Object.assign(create, { open: true, srcIds: [s.id], srcLabel: sourceValue(s), regions: [], vsyaRf: true, limit: 5, mask: 127 });
|
||||
Object.assign(create, { open: true, srcIds: [s.id], srcLabel: sourceValue(s), regions: [], vsyaRf: false, limit: 5, mask: 127 });
|
||||
}
|
||||
// Групповое создание: проекты для всех выбранных источников БЕЗ проекта — одним окном.
|
||||
function openBulkCreate() {
|
||||
const ids = selWork.value.filter((s) => !s.project).map((s) => s.id);
|
||||
if (!ids.length) return;
|
||||
regQuery.value = '';
|
||||
Object.assign(create, { open: true, srcIds: ids, srcLabel: `выбрано источников: ${ids.length}`, regions: [], vsyaRf: true, limit: 5, mask: 127 });
|
||||
Object.assign(create, { open: true, srcIds: ids, srcLabel: `выбрано источников: ${ids.length}`, regions: [], vsyaRf: false, limit: 5, mask: 127 });
|
||||
}
|
||||
function onCreateVsyaRf() {
|
||||
if (create.vsyaRf) create.regions = [];
|
||||
@@ -328,7 +329,7 @@ const regOpen = ref(false);
|
||||
const regFiltered = computed(() => {
|
||||
const q = regQuery.value.trim().toLowerCase();
|
||||
const chosen = new Set(create.regions);
|
||||
return regions.filter((r) => !chosen.has(r.code) && (q === '' || r.name.toLowerCase().includes(q))).slice(0, 8);
|
||||
return regions.filter((r) => !chosen.has(r.code) && (q === '' || r.name.toLowerCase().includes(q)));
|
||||
});
|
||||
function pickRegion(code: number) {
|
||||
if (!create.regions.includes(code)) create.regions.push(code);
|
||||
@@ -343,7 +344,7 @@ const setQuery = ref('');
|
||||
const setOpen = ref(false);
|
||||
const setFiltered = computed(() => {
|
||||
const q = setQuery.value.trim().toLowerCase();
|
||||
return regions.filter((r) => q === '' || r.name.toLowerCase().includes(q)).slice(0, 8);
|
||||
return regions.filter((r) => q === '' || r.name.toLowerCase().includes(q));
|
||||
});
|
||||
function pickSetRegion(code: number) {
|
||||
psettings.regionCode = code;
|
||||
@@ -712,6 +713,7 @@ defineExpose({ ctab, selected, inWork, props: props, toast, create, switchTab, t
|
||||
</template>
|
||||
<template v-else>
|
||||
<h3 class="ld-modal__h">Идёт изучение… <span class="ld-spin"></span></h3>
|
||||
<RunCollectStatus :run="store.currentRun" />
|
||||
<p class="ld-modal__m">Можно закрыть вкладку — сохраним результат. Деньги спишутся только при успехе.</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { autopodborErrorMessage, type FieldCompetitorDto, type DuplicateGroupDto
|
||||
import { REGIONS } from '../../../constants/regions';
|
||||
import { useOverlayDismiss } from '../../../composables/useOverlayDismiss';
|
||||
import CompetitorElementsEditor from './CompetitorElementsEditor.vue';
|
||||
import RunCollectStatus from './RunCollectStatus.vue';
|
||||
|
||||
interface AutopodborNav {
|
||||
go: (s: string) => void;
|
||||
@@ -459,6 +460,7 @@ defineExpose({ competitors, selected, toggle, toggleAll, bulkProjects, openColle
|
||||
</template>
|
||||
<template v-else>
|
||||
<h3 class="ld-modal__h">Идёт подбор… <span class="ld-spin"></span></h3>
|
||||
<RunCollectStatus :run="store.currentRun" />
|
||||
<p class="ld-modal__m">Можно закрыть вкладку — мы сохраним результат. Деньги спишутся только при успехе.</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue';
|
||||
import { useAutopodborStore } from '../../../stores/autopodborStore';
|
||||
import RunCollectStatus from './RunCollectStatus.vue';
|
||||
|
||||
const nav = inject('autopodborNav') as { go: (s: string) => void; ctx: any; screen: any };
|
||||
const store = useAutopodborStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -9,6 +12,7 @@ const nav = inject('autopodborNav') as { go: (s: string) => void; ctx: any; scre
|
||||
<div class="ld-loading-screen__wrap">
|
||||
<div class="ld-spin"></div>
|
||||
<p class="ld-loading-screen__msg">{{ nav.ctx.loadMsg || 'Идёт работа…' }}</p>
|
||||
<RunCollectStatus :run="store.currentRun" />
|
||||
<p class="ld-loading-screen__sub">{{ nav.ctx.loadSub || 'Пожалуйста, подождите.' }}</p>
|
||||
<p class="ld-loading-screen__note">
|
||||
Можно закрыть вкладку — мы сохраним результат и покажем его, когда будет готово.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import type { RunDto } from '../../../api/autopodbor';
|
||||
|
||||
// Что показать клиенту, пока идёт сбор: место в общей очереди (пока ждём запуска) ИЛИ текущий этап
|
||||
// (пока идёт работа). Очередь важнее — если прогон ещё не начали, показываем место.
|
||||
defineProps<{ run: RunDto | null }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p v-if="run && run.queue_position" class="ld-modal__m">
|
||||
Вы в очереди: <b>№{{ run.queue_position }}</b>. Как только дойдёт черёд — начнём.
|
||||
</p>
|
||||
<p v-else-if="run && run.progress" class="ld-modal__m">
|
||||
Этап {{ run.progress.stage }} из {{ run.progress.total }}: <b>{{ run.progress.label }}</b>
|
||||
</p>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import RunCollectStatus from '../../resources/js/views/autopodbor/screens/RunCollectStatus.vue';
|
||||
|
||||
describe('RunCollectStatus — что видит клиент во время сбора', () => {
|
||||
it('в очереди — показывает номер места', () => {
|
||||
const w = mount(RunCollectStatus, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
props: { run: { status: 'queued', queue_position: 3, progress: null } as any },
|
||||
});
|
||||
expect(w.text()).toContain('№3');
|
||||
expect(w.text().toLowerCase()).toContain('очеред');
|
||||
});
|
||||
|
||||
it('идёт работа — показывает «этап N из M» и подпись этапа', () => {
|
||||
const w = mount(RunCollectStatus, {
|
||||
props: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
run: { status: 'running', queue_position: null, progress: { stage: 2, total: 4, label: 'Ищем фирмы в справочниках' } } as any,
|
||||
},
|
||||
});
|
||||
const t = w.text();
|
||||
expect(t).toContain('2');
|
||||
expect(t).toContain('4');
|
||||
expect(t).toContain('Ищем фирмы в справочниках');
|
||||
});
|
||||
|
||||
it('очередь важнее этапа: если есть и место, и этап — показываем место', () => {
|
||||
const w = mount(RunCollectStatus, {
|
||||
props: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
run: { status: 'queued', queue_position: 5, progress: { stage: 1, total: 4, label: 'что-то' } as any } as any,
|
||||
},
|
||||
});
|
||||
expect(w.text()).toContain('№5');
|
||||
expect(w.text()).not.toContain('что-то');
|
||||
});
|
||||
|
||||
it('нет данных (run=null) — ничего не показывает и не падает', () => {
|
||||
const w = mount(RunCollectStatus, { props: { run: null } });
|
||||
expect(w.text().trim()).toBe('');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user