323323 from { opacity : 0 ; transform : translateY (4px ); }
324324 to { opacity : 1 ; transform : translateY (0 ); }
325325 }
326-
327- /* Toast уведомления */
328- @keyframes slideInRight {
329- from {
330- opacity : 0 ;
331- transform : translateX (100% );
332- }
333- to {
334- opacity : 1 ;
335- transform : translateX (0 );
336- }
337- }
338-
339- @keyframes slideOutRight {
340- from {
341- opacity : 1 ;
342- transform : translateX (0 );
343- }
344- to {
345- opacity : 0 ;
346- transform : translateX (100% );
347- }
348- }
349-
350- .toast-enter {
351- animation : slideInRight 0.4s cubic-bezier (0.68 , -0.55 , 0.265 , 1.55 );
352- }
353-
354- .toast-exit {
355- animation : slideOutRight 0.3s ease-in forwards;
356- }
357-
358- .toast-container {
359- position : fixed;
360- top : 20px ;
361- right : 20px ;
362- z-index : 9999 ;
363- display : flex;
364- flex-direction : column;
365- gap : 12px ;
366- pointer-events : none;
367- }
368-
369- .toast-notification {
370- pointer-events : auto;
371- min-width : 320px ;
372- max-width : 420px ;
373- border-radius : 12px ;
374- box-shadow : 0 10px 40px rgba (0 , 0 , 0 , 0.15 ), 0 4px 12px rgba (0 , 0 , 0 , 0.1 );
375- overflow : hidden;
376- }
377-
378- .toast-success {
379- background : linear-gradient (135deg , # 10b981 0% , # 059669 100% );
380- }
381-
382- .toast-error {
383- background : linear-gradient (135deg , # ef4444 0% , # dc2626 100% );
384- }
385-
386- .toast-info {
387- background : linear-gradient (135deg , # 3b82f6 0% , # 2563eb 100% );
388- }
389-
390- .toast-warning {
391- background : linear-gradient (135deg , # f59e0b 0% , # d97706 100% );
392- }
393326 </ style >
394327</ head >
395328< body class ="bg-gray-100 ">
@@ -450,39 +383,6 @@ <h1 class="text-xl font-bold text-gray-800">HH.ru Analytics</h1>
450383 </ div >
451384 </ nav >
452385
453- <!-- Toast уведомления -->
454- < div class ="toast-container ">
455- < transition name ="toast ">
456- < div v-if ="toast.visible "
457- class ="toast-notification toast-enter "
458- :class ="'toast-' + toast.type ">
459- < div class ="p-4 text-white ">
460- < div class ="flex items-start gap-3 ">
461- <!-- Иконка -->
462- < div class ="flex-shrink-0 w-10 h-10 rounded-lg bg-white/20 flex items-center justify-center ">
463- < i :class ="toastIcon " class ="text-xl "> </ i >
464- </ div >
465- <!-- Контент -->
466- < div class ="flex-1 min-w-0 ">
467- < h4 class ="font-bold text-sm mb-1 "> {{ toast.title }}</ h4 >
468- < p class ="text-xs text-white/90 leading-relaxed "> {{ toast.message }}</ p >
469- </ div >
470- <!-- Кнопка закрытия -->
471- < button @click ="hideToast "
472- class ="flex-shrink-0 text-white/80 hover:text-white hover:bg-white/20 rounded-lg p-1 transition ">
473- < i class ="fas fa-times "> </ i >
474- </ button >
475- </ div >
476- <!-- Прогресс-бар автозакрытия -->
477- < div class ="mt-3 w-full bg-white/20 rounded-full h-1 overflow-hidden ">
478- < div class ="bg-white h-1 rounded-full transition-all duration-100 "
479- :style ="{ width: toastProgress + '%', transitionDuration: '100ms' } "> </ div >
480- </ div >
481- </ div >
482- </ div >
483- </ transition >
484- </ div >
485-
486386 <!-- Модальное окно Onboarding Wizard -->
487387 < transition name ="fade ">
488388 < div v-if ="showOnboarding "
@@ -2930,17 +2830,6 @@ <h4 class="font-bold text-gray-900 mb-3 flex items-center">
29302830 loading : false ,
29312831 skeletonLoading : true , // Skeleton для дашборда
29322832
2933- // Toast уведомления
2934- toast : {
2935- visible : false ,
2936- type : 'success' , // success, error, info, warning
2937- title : '' ,
2938- message : ''
2939- } ,
2940- toastTimer : null ,
2941- toastProgress : 100 ,
2942- previousParserStatus : null , // Для отслеживания изменений
2943-
29442833 // Тренд зарплат
29452834 salaryTrend : [ ] ,
29462835
@@ -2964,17 +2853,6 @@ <h4 class="font-bold text-gray-900 mb-3 flex items-center">
29642853 } ,
29652854
29662855 computed : {
2967- // Иконка для toast-уведомления
2968- toastIcon ( ) {
2969- const icons = {
2970- 'success' : 'fas fa-check-circle' ,
2971- 'error' : 'fas fa-exclamation-circle' ,
2972- 'info' : 'fas fa-info-circle' ,
2973- 'warning' : 'fas fa-exclamation-triangle'
2974- } ;
2975- return icons [ this . toast . type ] || 'fas fa-bell' ;
2976- } ,
2977-
29782856 parserStatusColor ( ) {
29792857 const colors = {
29802858 'idle' : 'text-gray-600' ,
@@ -3107,82 +2985,6 @@ <h4 class="font-bold text-gray-900 mb-3 flex items-center">
31072985 } ,
31082986
31092987 methods : {
3110- // ==================== TOAST УВЕДОМЛЕНИЯ ====================
3111-
3112- // Показать toast-уведомление
3113- showToast ( type , title , message , duration = 5000 ) {
3114- // Скрыть текущее уведомление если есть
3115- if ( this . toastTimer ) {
3116- clearTimeout ( this . toastTimer ) ;
3117- }
3118-
3119- this . toast = {
3120- visible : true ,
3121- type : type ,
3122- title : title ,
3123- message : message
3124- } ;
3125- this . toastProgress = 100 ;
3126-
3127- // Анимация прогресс-бара
3128- const step = 100 / ( duration / 100 ) ;
3129- const progressInterval = setInterval ( ( ) => {
3130- this . toastProgress = Math . max ( 0 , this . toastProgress - step ) ;
3131- } , 100 ) ;
3132-
3133- // Автозакрытие
3134- this . toastTimer = setTimeout ( ( ) => {
3135- clearInterval ( progressInterval ) ;
3136- this . hideToast ( ) ;
3137- } , duration ) ;
3138- } ,
3139-
3140- // Скрыть toast-уведомление
3141- hideToast ( ) {
3142- this . toast . visible = false ;
3143- this . toastProgress = 100 ;
3144- if ( this . toastTimer ) {
3145- clearTimeout ( this . toastTimer ) ;
3146- this . toastTimer = null ;
3147- }
3148- } ,
3149-
3150- // Проверить статус парсера и показать уведомление при изменении
3151- checkParserStatusChange ( ) {
3152- const currentStatus = this . parserState . status ;
3153- const previousStatus = this . previousParserStatus ;
3154-
3155- // Если статус изменился с running на completed/error/stopped
3156- if ( previousStatus === 'running' && currentStatus !== 'running' ) {
3157- if ( currentStatus === 'completed' ) {
3158- const count = this . parserState . vacancies_collected || 0 ;
3159- this . showToast (
3160- 'success' ,
3161- '✅ Парсинг завершён!' ,
3162- `Успешно собрано ${ count } вакансий. Перейдите на вкладку «Аналитика» для просмотра результатов.` ,
3163- 6000
3164- ) ;
3165- } else if ( currentStatus === 'error' ) {
3166- this . showToast (
3167- 'error' ,
3168- '❌ Ошибка парсинга' ,
3169- this . parserState . error_message || 'Произошла ошибка при сборе данных.' ,
3170- 6000
3171- ) ;
3172- } else if ( currentStatus === 'stopped' ) {
3173- this . showToast (
3174- 'warning' ,
3175- '⏹️ Парсинг остановлен' ,
3176- 'Сбор данных был остановлен пользователем.' ,
3177- 4000
3178- ) ;
3179- }
3180- }
3181-
3182- // Обновить предыдущий статус
3183- this . previousParserStatus = currentStatus ;
3184- } ,
3185-
31862988 // ==================== ONBOARDING WIZARD ====================
31872989
31882990 // Добавить ключевое слово из быстрых подсказок
@@ -3687,15 +3489,9 @@ <h4 class="font-bold text-gray-900 mb-3 flex items-center">
36873489
36883490 async loadParserStatus ( ) {
36893491 try {
3690- const oldStatus = this . parserState . status ;
36913492 const response = await fetch ( `${ API_BASE } /parser/status` ) ;
36923493 const data = await response . json ( ) ;
36933494 this . parserState = data . state ;
3694-
3695- // Проверяем изменение статуса сразу после загрузки
3696- if ( oldStatus && this . parserState . status !== oldStatus ) {
3697- this . checkParserStatusChange ( ) ;
3698- }
36993495 } catch ( error ) {
37003496 console . error ( 'Ошибка загрузки статуса парсера:' , error ) ;
37013497 }
0 commit comments