mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
remove app from repos
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
|
||||
services:
|
||||
frontend:
|
||||
image: node:20-alpine
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ./:/app
|
||||
# Исключаем node_modules контейнера, чтобы не конфликтовать с хостом
|
||||
# (хотя для dev-режима часто работает и сквозное монтирование)
|
||||
- /app/node_modules
|
||||
ports:
|
||||
- "5173:5173"
|
||||
# Для Windows/Mac, чтобы видеть бэкенд на хосте
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
command: npm run dev
|
||||
@@ -1,12 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>RMSER App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
2488
rmser-app/package-lock.json
generated
2488
rmser-app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "rmser-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"axios": "^1.6.0",
|
||||
"clsx": "^2.0.0",
|
||||
"lucide-react": "^0.292.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.18.0",
|
||||
"tailwind-merge": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"postcss": "^8.4.31",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.5"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { Layout } from './components/Layout';
|
||||
import { Dashboard } from './pages/Dashboard';
|
||||
import { OcrTraining } from './pages/OcrTraining';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route index element={<Dashboard />} />
|
||||
<Route path="ocr" element={<OcrTraining />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,17 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export const apiClient = axios.create({
|
||||
baseURL: '/api', // Vite прокси перенаправит это на :8080
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Интерцептор для обработки ошибок (опционально)
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
console.error('API Error:', error.response?.data || error.message);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
@@ -1,51 +0,0 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiClient } from './client';
|
||||
import { CatalogItem, ProductMatch, Recommendation, CreateMatchRequest } from './types';
|
||||
|
||||
// --- Recommendations ---
|
||||
export const useRecommendations = () => {
|
||||
return useQuery({
|
||||
queryKey: ['recommendations'],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get<Recommendation[]>('/recommendations');
|
||||
return data;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// --- OCR ---
|
||||
export const useCatalog = () => {
|
||||
return useQuery({
|
||||
queryKey: ['catalog'],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get<CatalogItem[]>('/ocr/catalog');
|
||||
return data;
|
||||
},
|
||||
staleTime: 1000 * 60 * 5, // Каталог меняется редко, кэшируем на 5 минут
|
||||
});
|
||||
};
|
||||
|
||||
export const useMatches = () => {
|
||||
return useQuery({
|
||||
queryKey: ['matches'],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get<ProductMatch[]>('/ocr/matches');
|
||||
return data;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateMatch = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (payload: CreateMatchRequest) => {
|
||||
const { data } = await apiClient.post('/ocr/match', payload);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Обновляем список матчей после добавления
|
||||
queryClient.invalidateQueries({ queryKey: ['matches'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
export type UUID = string;
|
||||
|
||||
// --- Каталог и OCR ---
|
||||
export interface CatalogItem {
|
||||
id: UUID;
|
||||
name: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface ProductMatch {
|
||||
raw_name: string;
|
||||
product_id: UUID;
|
||||
product?: CatalogItem;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateMatchRequest {
|
||||
raw_name: string;
|
||||
product_id: UUID;
|
||||
}
|
||||
|
||||
// --- Рекомендации ---
|
||||
export interface Recommendation {
|
||||
ID: UUID;
|
||||
Type: string;
|
||||
ProductID: UUID;
|
||||
ProductName: string;
|
||||
Reason: string;
|
||||
CreatedAt: string;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Link, Outlet, useLocation } from 'react-router-dom';
|
||||
import { LayoutDashboard, BrainCircuit, Menu } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ label: 'Аналитика', path: '/', icon: LayoutDashboard },
|
||||
{ label: 'Обучение OCR', path: '/ocr', icon: BrainCircuit },
|
||||
];
|
||||
|
||||
export const Layout = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 flex flex-col md:flex-row font-sans text-slate-900">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-full md:w-64 bg-white border-r border-slate-200 flex-shrink-0">
|
||||
<div className="h-16 flex items-center px-6 border-b border-slate-100">
|
||||
<span className="text-xl font-bold text-indigo-600">RMSER</span>
|
||||
<span className="ml-2 text-xs bg-indigo-50 text-indigo-700 px-2 py-0.5 rounded">v1.0</span>
|
||||
</div>
|
||||
|
||||
<nav className="p-4 space-y-1">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = location.pathname === item.path;
|
||||
return (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={clsx(
|
||||
"flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium transition-colors",
|
||||
isActive
|
||||
? "bg-indigo-50 text-indigo-700"
|
||||
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900"
|
||||
)}
|
||||
>
|
||||
<Icon size={18} />
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 overflow-auto">
|
||||
<header className="h-16 bg-white border-b border-slate-200 flex items-center px-6 md:hidden">
|
||||
<Menu className="text-slate-500" />
|
||||
<span className="ml-3 font-semibold">Меню</span>
|
||||
</header>
|
||||
<div className="p-4 md:p-8 max-w-7xl mx-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,87 +0,0 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { CatalogItem } from '../api/types';
|
||||
import { ChevronsUpDown, Check } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface Props {
|
||||
items: CatalogItem[];
|
||||
value: string | null; // ID выбранного товара
|
||||
onChange: (id: string) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export const ProductSelect = ({ items, value, onChange, isLoading }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
const selectedItem = items.find((i) => i.id === value);
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
if (!query) return items.slice(0, 50); // Показываем первые 50 для скорости
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return items.filter((item) =>
|
||||
item.name.toLowerCase().includes(lowerQuery) ||
|
||||
item.code.includes(lowerQuery)
|
||||
).slice(0, 50);
|
||||
}, [items, query]);
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<div
|
||||
className="flex items-center justify-between w-full px-3 py-2 text-sm bg-white border rounded-md border-slate-300 cursor-pointer focus-within:ring-2 focus-within:ring-indigo-500 focus-within:border-transparent"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<span className={clsx("block truncate", !selectedItem && "text-slate-400")}>
|
||||
{selectedItem ? selectedItem.name : "Выберите товар..."}
|
||||
</span>
|
||||
<ChevronsUpDown className="w-4 h-4 opacity-50" />
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-10"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
<div className="absolute z-20 w-full mt-1 bg-white border border-slate-200 rounded-md shadow-lg max-h-60 overflow-auto">
|
||||
<div className="p-2 sticky top-0 bg-white border-b">
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-2 py-1 text-sm border rounded bg-slate-50 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||
placeholder="Поиск по названию или коду..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="p-4 text-center text-sm text-slate-500">Загрузка каталога...</div>
|
||||
) : filteredItems.length === 0 ? (
|
||||
<div className="p-4 text-center text-sm text-slate-500">Ничего не найдено</div>
|
||||
) : (
|
||||
<ul>
|
||||
{filteredItems.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className={clsx(
|
||||
"px-3 py-2 text-sm cursor-pointer hover:bg-indigo-50 flex items-center justify-between",
|
||||
item.id === value && "bg-indigo-50 text-indigo-700 font-medium"
|
||||
)}
|
||||
onClick={() => {
|
||||
onChange(item.id);
|
||||
setIsOpen(false);
|
||||
setQuery('');
|
||||
}}
|
||||
>
|
||||
<span>{item.name}</span>
|
||||
{item.id === value && <Check className="w-4 h-4" />}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import App from './App';
|
||||
import './index.css'; // Tailwind directives here
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false, // Отключаем лишние запросы при переключении вкладок
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -1,88 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useRecommendations } from '../api/queries';
|
||||
import { AlertCircle, PackageX, FileWarning } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
// Маппинг типов ошибок на заголовки и цвета (для красоты)
|
||||
const TYPE_CONFIG: Record<string, { label: string; color: string; icon: any }> = {
|
||||
UNUSED_IN_RECIPES: {
|
||||
label: 'Товары без техкарт',
|
||||
color: 'bg-amber-50 text-amber-700 border-amber-200',
|
||||
icon: FileWarning
|
||||
},
|
||||
NO_INCOMING: {
|
||||
label: 'Ингредиенты без закупок',
|
||||
color: 'bg-red-50 text-red-700 border-red-200',
|
||||
icon: PackageX
|
||||
},
|
||||
// Default
|
||||
DEFAULT: {
|
||||
label: 'Прочие предупреждения',
|
||||
color: 'bg-slate-50 text-slate-700 border-slate-200',
|
||||
icon: AlertCircle
|
||||
}
|
||||
};
|
||||
|
||||
export const Dashboard = () => {
|
||||
const { data: recommendations, isLoading, isError } = useRecommendations();
|
||||
|
||||
// Группировка данных по Type
|
||||
const groupedData = useMemo(() => {
|
||||
if (!recommendations) return {};
|
||||
return recommendations.reduce((acc, item) => {
|
||||
const key = item.Type;
|
||||
if (!acc[key]) acc[key] = [];
|
||||
acc[key].push(item);
|
||||
return acc;
|
||||
}, {} as Record<string, typeof recommendations>);
|
||||
}, [recommendations]);
|
||||
|
||||
if (isLoading) return <div className="p-8 text-center text-slate-500">Загрузка аналитики...</div>;
|
||||
if (isError) return <div className="p-8 text-center text-red-500">Ошибка загрузки данных</div>;
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">Дашборд аналитики</h1>
|
||||
<p className="text-slate-500">Проблемы учета и рекомендации по складу</p>
|
||||
</div>
|
||||
|
||||
{Object.entries(groupedData).map(([type, items]) => {
|
||||
const config = TYPE_CONFIG[type] || TYPE_CONFIG.DEFAULT;
|
||||
const Icon = config.icon;
|
||||
|
||||
return (
|
||||
<div key={type} className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className={clsx("w-5 h-5", config.color.split(' ')[1])} />
|
||||
<h2 className="text-lg font-semibold text-slate-800">
|
||||
{config.label} <span className="text-slate-400 text-sm ml-1">({items.length})</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{items.map((rec) => (
|
||||
<div
|
||||
key={rec.ID}
|
||||
className="bg-white p-4 rounded-lg border border-slate-200 shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="font-medium text-slate-900 mb-1">{rec.ProductName}</div>
|
||||
<div className="text-sm text-slate-600 mb-3">{rec.Reason}</div>
|
||||
<div className="text-xs text-slate-400">
|
||||
Обнаружено: {new Date(rec.CreatedAt).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{(!recommendations || recommendations.length === 0) && (
|
||||
<div className="text-center py-10 bg-white rounded-lg border border-slate-200">
|
||||
<p className="text-green-600 font-medium">Отлично! Проблем не обнаружено.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,130 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useCatalog, useMatches, useCreateMatch } from '../api/queries';
|
||||
import { ProductSelect } from '../components/ProductSelect';
|
||||
import { Plus, Save, Loader2 } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export const OcrTraining = () => {
|
||||
// Queries
|
||||
const { data: matches, isLoading: loadingMatches } = useMatches();
|
||||
const { data: catalog, isLoading: loadingCatalog } = useCatalog();
|
||||
const createMatchMutation = useCreateMatch();
|
||||
|
||||
// Form State
|
||||
const [rawName, setRawName] = useState('');
|
||||
const [selectedProductId, setSelectedProductId] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!rawName || !selectedProductId) return;
|
||||
|
||||
createMatchMutation.mutate(
|
||||
{ raw_name: rawName, product_id: selectedProductId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setRawName('');
|
||||
setSelectedProductId(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">Обучение OCR</h1>
|
||||
<p className="text-slate-500">Связывание наименований из чеков с номенклатурой iiko</p>
|
||||
</div>
|
||||
|
||||
{/* Форма добавления */}
|
||||
<div className="bg-white p-6 rounded-lg border border-slate-200 shadow-sm">
|
||||
<h3 className="text-sm font-semibold text-slate-900 uppercase tracking-wider mb-4 flex items-center gap-2">
|
||||
<Plus className="w-4 h-4" /> Добавить правило
|
||||
</h3>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col md:flex-row gap-4 items-end">
|
||||
<div className="w-full md:w-1/2 space-y-1.5">
|
||||
<label className="text-sm font-medium text-slate-700">Текст из чека (Raw)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={rawName}
|
||||
onChange={(e) => setRawName(e.target.value)}
|
||||
placeholder="Например: Молоко 3.2 петмол"
|
||||
className="w-full px-3 py-2 text-sm border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-1/2 space-y-1.5">
|
||||
<label className="text-sm font-medium text-slate-700">Товар в iiko</label>
|
||||
<ProductSelect
|
||||
items={catalog || []}
|
||||
value={selectedProductId}
|
||||
onChange={setSelectedProductId}
|
||||
isLoading={loadingCatalog}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!rawName || !selectedProductId || createMatchMutation.isPending}
|
||||
className={clsx(
|
||||
"px-4 py-2 rounded-md text-sm font-medium text-white transition-colors flex items-center gap-2 min-w-[120px] justify-center",
|
||||
(!rawName || !selectedProductId)
|
||||
? "bg-slate-300 cursor-not-allowed"
|
||||
: "bg-indigo-600 hover:bg-indigo-700"
|
||||
)}
|
||||
>
|
||||
{createMatchMutation.isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||
Сохранить
|
||||
</button>
|
||||
</form>
|
||||
{createMatchMutation.isError && (
|
||||
<p className="mt-2 text-sm text-red-500">Ошибка при сохранении. Возможно, такая связь уже есть.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Таблица существующих связей */}
|
||||
<div className="bg-white border border-slate-200 rounded-lg shadow-sm overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-slate-200 bg-slate-50">
|
||||
<h3 className="font-semibold text-slate-800">Активные связи</h3>
|
||||
</div>
|
||||
|
||||
{loadingMatches ? (
|
||||
<div className="p-8 text-center text-slate-500">Загрузка данных...</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="bg-slate-50 text-slate-500 font-medium">
|
||||
<tr>
|
||||
<th className="px-6 py-3">Текст из чека</th>
|
||||
<th className="px-6 py-3">Товар iiko</th>
|
||||
<th className="px-6 py-3">Дата обновления</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{matches?.map((match) => (
|
||||
<tr key={match.raw_name} className="hover:bg-slate-50">
|
||||
<td className="px-6 py-3 font-medium text-slate-900">{match.raw_name}</td>
|
||||
<td className="px-6 py-3 text-indigo-600">
|
||||
{match.product?.name || match.product_id}
|
||||
</td>
|
||||
<td className="px-6 py-3 text-slate-400">
|
||||
{new Date(match.updated_at).toLocaleDateString()}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{matches?.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={3} className="px-6 py-8 text-center text-slate-500">
|
||||
Список пуст. Добавьте первое правило обучения.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://host.docker.internal:8080',
|
||||
changeOrigin: true,
|
||||
// Если бэкенд ожидает /api, rewrite не нужен.
|
||||
// Если бэкенд на корне (localhost:8080/recommendations), то раскомментируй:
|
||||
// rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user