mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
0302-отрефакторил в нормальный вид на мобилу и десктоп
сразу выкинул пути в импортах и добавил алиас для корня
This commit is contained in:
75
rmser-view/src/shared/hooks/useOcr.ts
Normal file
75
rmser-view/src/shared/hooks/useOcr.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '../api';
|
||||
import type { MatchRequest, ProductMatch, CatalogItem, UnmatchedItem } from '../types';
|
||||
import { message } from 'antd';
|
||||
|
||||
export const useOcr = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const catalogQuery = useQuery<CatalogItem[], Error>({
|
||||
queryKey: ['catalog'],
|
||||
queryFn: api.getCatalogItems,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
});
|
||||
|
||||
const matchesQuery = useQuery<ProductMatch[], Error>({
|
||||
queryKey: ['matches'],
|
||||
queryFn: api.getMatches,
|
||||
});
|
||||
|
||||
const unmatchedQuery = useQuery<UnmatchedItem[], Error>({
|
||||
queryKey: ['unmatched'],
|
||||
queryFn: api.getUnmatched,
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
const createMatchMutation = useMutation({
|
||||
// Теперь типы совпадают, any не нужен
|
||||
mutationFn: (newMatch: MatchRequest) => api.createMatch(newMatch),
|
||||
onSuccess: () => {
|
||||
message.success('Связь сохранена');
|
||||
queryClient.invalidateQueries({ queryKey: ['matches'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['unmatched'] });
|
||||
},
|
||||
onError: () => {
|
||||
message.error('Ошибка при сохранении');
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMatchMutation = useMutation({
|
||||
mutationFn: (rawName: string) => api.deleteMatch(rawName),
|
||||
onSuccess: () => {
|
||||
message.success('Связь удалена');
|
||||
queryClient.invalidateQueries({ queryKey: ['matches'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['unmatched'] });
|
||||
},
|
||||
onError: () => {
|
||||
message.error('Ошибка при удалении связи');
|
||||
},
|
||||
});
|
||||
|
||||
const deleteUnmatchedMutation = useMutation({
|
||||
mutationFn: (rawName: string) => api.deleteUnmatched(rawName),
|
||||
onSuccess: () => {
|
||||
message.success('Нераспознанная строка удалена');
|
||||
queryClient.invalidateQueries({ queryKey: ['unmatched'] });
|
||||
},
|
||||
onError: () => {
|
||||
message.error('Ошибка при удалении нераспознанной строки');
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
catalog: catalogQuery.data || [],
|
||||
matches: matchesQuery.data || [],
|
||||
unmatched: unmatchedQuery.data || [],
|
||||
isLoading: catalogQuery.isPending || matchesQuery.isPending,
|
||||
isError: catalogQuery.isError || matchesQuery.isError,
|
||||
createMatch: createMatchMutation.mutate,
|
||||
isCreating: createMatchMutation.isPending,
|
||||
deleteMatch: deleteMatchMutation.mutate,
|
||||
isDeletingMatch: deleteMatchMutation.isPending,
|
||||
deleteUnmatched: deleteUnmatchedMutation.mutate,
|
||||
isDeletingUnmatched: deleteUnmatchedMutation.isPending,
|
||||
};
|
||||
};
|
||||
33
rmser-view/src/shared/hooks/usePlatform.ts
Normal file
33
rmser-view/src/shared/hooks/usePlatform.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export type Platform = 'MobileApp' | 'Desktop' | 'MobileBrowser';
|
||||
|
||||
/**
|
||||
* Хук для определения текущей платформы
|
||||
* MobileApp - если есть специфические признаки мобильного приложения
|
||||
* Desktop - если это десктопный браузер
|
||||
* MobileBrowser - если это мобильный браузер
|
||||
*/
|
||||
export const usePlatform = (): Platform => {
|
||||
return useMemo(() => {
|
||||
const userAgent = navigator.userAgent;
|
||||
|
||||
// Проверка на мобильное приложение (специфические признаки)
|
||||
// Можно добавить дополнительные проверки для конкретных приложений
|
||||
const isMobileApp = /rmser-app|mobile-app|cordova|phonegap/i.test(userAgent);
|
||||
|
||||
if (isMobileApp) {
|
||||
return 'MobileApp';
|
||||
}
|
||||
|
||||
// Проверка на мобильный браузер
|
||||
const isMobileBrowser = /android|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent);
|
||||
|
||||
if (isMobileBrowser) {
|
||||
return 'MobileBrowser';
|
||||
}
|
||||
|
||||
// По умолчанию - десктоп
|
||||
return 'Desktop';
|
||||
}, []);
|
||||
};
|
||||
12
rmser-view/src/shared/hooks/useRecommendations.ts
Normal file
12
rmser-view/src/shared/hooks/useRecommendations.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '../api';
|
||||
import type { Recommendation } from '../types';
|
||||
|
||||
export const useRecommendations = () => {
|
||||
return useQuery<Recommendation[], Error>({
|
||||
queryKey: ['recommendations'],
|
||||
queryFn: api.getRecommendations,
|
||||
// Обновлять данные каждые 30 секунд, если вкладка активна
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
};
|
||||
63
rmser-view/src/shared/hooks/useUserRole.ts
Normal file
63
rmser-view/src/shared/hooks/useUserRole.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { api } from '../api';
|
||||
import type { UserSettings, UserRole } from '../types';
|
||||
|
||||
interface UseUserRoleResult {
|
||||
/** Роль текущего пользователя или null если не загружено */
|
||||
role: UserRole | null;
|
||||
/** Полные настройки пользователя */
|
||||
settings: UserSettings | null;
|
||||
/** Состояние загрузки */
|
||||
loading: boolean;
|
||||
/** Ошибка загрузки */
|
||||
error: string | null;
|
||||
/** Функция для повторной загрузки настроек */
|
||||
refetch: () => Promise<void>;
|
||||
/** Является ли пользователь оператором */
|
||||
isOperator: boolean;
|
||||
/** Является ли пользователь админом или владельцем */
|
||||
isAdminOrOwner: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Хук для получения роли пользователя и настроек.
|
||||
* Автоматически загружает настройки при монтировании компонента.
|
||||
*/
|
||||
export const useUserRole = (): UseUserRoleResult => {
|
||||
const [settings, setSettings] = useState<UserSettings | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchSettings = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await api.getSettings();
|
||||
setSettings(data);
|
||||
} catch (err) {
|
||||
console.error('Ошибка при загрузке настроек:', err);
|
||||
setError(err instanceof Error ? err.message : 'Не удалось загрузить настройки');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Загружаем настройки при монтировании
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, [fetchSettings]);
|
||||
|
||||
const role = settings?.role ?? null;
|
||||
const isOperator = role === 'OPERATOR';
|
||||
const isAdminOrOwner = role === 'ADMIN' || role === 'OWNER';
|
||||
|
||||
return {
|
||||
role,
|
||||
settings,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchSettings,
|
||||
isOperator,
|
||||
isAdminOrOwner,
|
||||
};
|
||||
};
|
||||
161
rmser-view/src/shared/hooks/useWebSocket.ts
Normal file
161
rmser-view/src/shared/hooks/useWebSocket.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||
|
||||
const apiUrl = import.meta.env.VITE_API_URL || '';
|
||||
|
||||
// Определяем базовый URL для WS (меняем http->ws, https->wss)
|
||||
const getWsUrl = () => {
|
||||
let baseUrl = apiUrl;
|
||||
if (baseUrl.startsWith('/')) {
|
||||
baseUrl = window.location.origin;
|
||||
} else if (!baseUrl) {
|
||||
baseUrl = 'http://localhost:8080';
|
||||
}
|
||||
|
||||
// Заменяем протокол
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const host = baseUrl.replace(/^http(s)?:\/\//, '');
|
||||
// Важно: путь /socket.io/ оставлен для совместимости с Nginx конфигом
|
||||
return `${protocol}//${host}/socket.io/`;
|
||||
};
|
||||
|
||||
interface WsEvent {
|
||||
event: string;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
interface UseWebSocketParams {
|
||||
autoReconnect?: boolean;
|
||||
onDisconnect?: () => void;
|
||||
}
|
||||
|
||||
export const useWebSocket = (sessionId: string | null, params: UseWebSocketParams = {}) => {
|
||||
const { autoReconnect = false, onDisconnect } = params;
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [lastError, setLastError] = useState<string | null>(null);
|
||||
const [lastMessage, setLastMessage] = useState<WsEvent | null>(null);
|
||||
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const reconnectAttemptsRef = useRef(0);
|
||||
const currentSessionIdRef = useRef<string | null>(null);
|
||||
|
||||
// Используем ref для хранения функции reconnect, чтобы она могла вызывать сама себя
|
||||
const reconnectRef = useRef<(() => void) | null>(null);
|
||||
|
||||
// Функция для переподключения WebSocket
|
||||
const reconnect = useCallback(() => {
|
||||
if (!currentSessionIdRef.current) return;
|
||||
|
||||
const delay = Math.min(1000 + reconnectAttemptsRef.current * 1000, 5000);
|
||||
reconnectAttemptsRef.current++;
|
||||
|
||||
console.log(`🔄 WS Reconnect attempt ${reconnectAttemptsRef.current} in ${delay}ms`);
|
||||
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current);
|
||||
}
|
||||
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
const url = `${getWsUrl()}?session_id=${currentSessionIdRef.current}`;
|
||||
console.log('🔌 Reconnecting WS:', url);
|
||||
|
||||
const ws = new WebSocket(url);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('✅ WS Reconnected');
|
||||
setIsConnected(true);
|
||||
setLastError(null);
|
||||
reconnectAttemptsRef.current = 0;
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
// Используем ref для вызова reconnect
|
||||
reconnectRef.current?.();
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('❌ WS Error', error);
|
||||
setLastError('Connection error');
|
||||
setIsConnected(false);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const parsed = JSON.parse(event.data);
|
||||
console.log('📨 WS Message:', parsed);
|
||||
setLastMessage(parsed);
|
||||
} catch {
|
||||
console.error('Failed to parse WS message', event.data);
|
||||
}
|
||||
};
|
||||
}, delay);
|
||||
}, []);
|
||||
|
||||
const handleDisconnect = useCallback(() => {
|
||||
if (onDisconnect) {
|
||||
onDisconnect();
|
||||
}
|
||||
|
||||
if (autoReconnect) {
|
||||
reconnect();
|
||||
}
|
||||
}, [onDisconnect, reconnect, autoReconnect]);
|
||||
|
||||
useEffect(() => {
|
||||
// Сохраняем reconnect в ref после его создания
|
||||
reconnectRef.current = reconnect;
|
||||
}, [reconnect]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sessionId) return;
|
||||
|
||||
currentSessionIdRef.current = sessionId;
|
||||
reconnectAttemptsRef.current = 0;
|
||||
|
||||
const url = `${getWsUrl()}?session_id=${sessionId}`;
|
||||
console.log('🔌 Connecting WS:', url);
|
||||
|
||||
const ws = new WebSocket(url);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('✅ WS Connected');
|
||||
setIsConnected(true);
|
||||
setLastError(null);
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log('⚠️ WS Closed', event.code, event.reason);
|
||||
setIsConnected(false);
|
||||
handleDisconnect();
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('❌ WS Error', error);
|
||||
setLastError('Connection error');
|
||||
setIsConnected(false);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const parsed = JSON.parse(event.data);
|
||||
console.log('📨 WS Message:', parsed);
|
||||
setLastMessage(parsed);
|
||||
} catch {
|
||||
console.error('Failed to parse WS message', event.data);
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
console.log('🧹 WS Cleanup');
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current);
|
||||
reconnectTimeoutRef.current = null;
|
||||
}
|
||||
ws.close();
|
||||
};
|
||||
}, [sessionId, handleDisconnect]);
|
||||
|
||||
return { isConnected, lastError, lastMessage };
|
||||
};
|
||||
Reference in New Issue
Block a user