main
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
50
README.md
Normal file
50
README.md
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||||
|
|
||||||
|
- Configure the top-level `parserOptions` property like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default tseslint.config({
|
||||||
|
languageOptions: {
|
||||||
|
// other options...
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
||||||
|
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
||||||
|
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import react from 'eslint-plugin-react'
|
||||||
|
|
||||||
|
export default tseslint.config({
|
||||||
|
// Set the react version
|
||||||
|
settings: { react: { version: '18.3' } },
|
||||||
|
plugins: {
|
||||||
|
// Add the react plugin
|
||||||
|
react,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// other rules...
|
||||||
|
// Enable its recommended rules
|
||||||
|
...react.configs.recommended.rules,
|
||||||
|
...react.configs['jsx-runtime'].rules,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
28
eslint.config.js
Normal file
28
eslint.config.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/fuel.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Топливо и Транспорт</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4933
package-lock.json
generated
Normal file
4933
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
57
package.json
Normal file
57
package.json
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"name": "fuel_react",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mantine/carousel": "^7.15.3",
|
||||||
|
"@mantine/charts": "^7.15.3",
|
||||||
|
"@mantine/code-highlight": "^7.15.3",
|
||||||
|
"@mantine/core": "^7.15.3",
|
||||||
|
"@mantine/dates": "^7.15.3",
|
||||||
|
"@mantine/dropzone": "^7.15.3",
|
||||||
|
"@mantine/form": "^7.15.3",
|
||||||
|
"@mantine/hooks": "^7.15.3",
|
||||||
|
"@mantine/modals": "^7.15.3",
|
||||||
|
"@mantine/notifications": "^7.15.3",
|
||||||
|
"@mantine/nprogress": "^7.15.3",
|
||||||
|
"@mantine/spotlight": "^7.15.3",
|
||||||
|
"@mantine/tiptap": "^7.15.3",
|
||||||
|
"@tabler/icons-react": "^3.28.1",
|
||||||
|
"@tiptap/extension-link": "^2.11.2",
|
||||||
|
"@tiptap/pm": "^2.11.2",
|
||||||
|
"@tiptap/react": "^2.11.2",
|
||||||
|
"@tiptap/starter-kit": "^2.11.2",
|
||||||
|
"axios": "^1.7.9",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
|
"embla-carousel-react": "^7.1.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-hook-form": "^7.60.0",
|
||||||
|
"react-imask": "^7.6.1",
|
||||||
|
"react-router-dom": "^7.1.1",
|
||||||
|
"recharts": "^2.15.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.17.0",
|
||||||
|
"@types/react": "^18.3.18",
|
||||||
|
"@types/react-dom": "^18.3.5",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"eslint": "^9.17.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.16",
|
||||||
|
"globals": "^15.14.0",
|
||||||
|
"postcss": "^8.5.0",
|
||||||
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
|
"postcss-simple-vars": "^7.0.1",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"typescript-eslint": "^8.18.2",
|
||||||
|
"vite": "^6.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
postcss.config.cjs
Normal file
14
postcss.config.cjs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
'postcss-preset-mantine': {},
|
||||||
|
'postcss-simple-vars': {
|
||||||
|
variables: {
|
||||||
|
'mantine-breakpoint-xs': '36em',
|
||||||
|
'mantine-breakpoint-sm': '48em',
|
||||||
|
'mantine-breakpoint-md': '62em',
|
||||||
|
'mantine-breakpoint-lg': '75em',
|
||||||
|
'mantine-breakpoint-xl': '88em',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
BIN
public/fuel.png
Normal file
BIN
public/fuel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
24
src/App.tsx
Normal file
24
src/App.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { MantineProvider } from "@mantine/core";
|
||||||
|
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||||
|
import { Layout } from "./components/Layout";
|
||||||
|
import Dictionaries from "./pages/Dictionaries";
|
||||||
|
import DriverForm from "./pages/DriverForm";
|
||||||
|
import 'dayjs/locale/ru'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<MantineProvider>
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route element={<Layout />}>
|
||||||
|
<Route path="/" element={<div>a</div>} />
|
||||||
|
<Route path="/dictionaries" element={<Dictionaries />} />
|
||||||
|
<Route path="/drivers" element={<DriverForm />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
</MantineProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
22
src/api/api.ts
Normal file
22
src/api/api.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const API_BASE_URL = "https://api.jkhsakha.ru/is/fuel";
|
||||||
|
|
||||||
|
export const fetchDictionary = async (directory: string) => {
|
||||||
|
const response = await axios.get(`${API_BASE_URL}/${directory}?limit=100`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createDictionaryItem = async (directory: string, data: any) => {
|
||||||
|
const response = await axios.post(`${API_BASE_URL}/${directory}`, data);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateDictionaryItem = async (directory: string, id: number, data: any) => {
|
||||||
|
const response = await axios.patch(`${API_BASE_URL}/${directory}/${id}`, data);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteDictionaryItem = async (directory: string, id: number) => {
|
||||||
|
await axios.delete(`${API_BASE_URL}/${directory}/${id}`);
|
||||||
|
};
|
||||||
67
src/components/DictionaryModal.tsx
Normal file
67
src/components/DictionaryModal.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Modal, Button, TextInput, Select } from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { fetchDictionary } from "../api/api"; // Импорт API запроса
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
opened: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (values: any) => void;
|
||||||
|
initialValues: any;
|
||||||
|
fields: { name: string; label: string; type: "text" | "select"; options?: any[] }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const DictionaryModal: React.FC<Props> = ({ opened, onClose, onSubmit, initialValues, fields }) => {
|
||||||
|
const form = useForm({ initialValues });
|
||||||
|
const [fuelTypes, setFuelTypes] = useState<{ value: string; label: string }[]>([]);
|
||||||
|
|
||||||
|
// Загружаем виды топлива при открытии модального окна
|
||||||
|
useEffect(() => {
|
||||||
|
if (opened) {
|
||||||
|
fetchDictionary("fuel_types").then((data) => {
|
||||||
|
const options = data.map((item: any) => ({
|
||||||
|
value: String(item.id), // Преобразуем ID в строку
|
||||||
|
label: item.name,
|
||||||
|
}));
|
||||||
|
setFuelTypes(options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [opened]);
|
||||||
|
|
||||||
|
// Обновляем форму при изменении initialValues
|
||||||
|
useEffect(() => {
|
||||||
|
const updatedValues = { ...initialValues };
|
||||||
|
|
||||||
|
// Приводим id_fuel_type к строке, если он есть
|
||||||
|
if (updatedValues.id_fuel_type !== undefined) {
|
||||||
|
updatedValues.id_fuel_type = String(updatedValues.id_fuel_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
form.setValues(updatedValues);
|
||||||
|
}, [initialValues]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal opened={opened} onClose={onClose} title="Добавить/Редактировать" centered>
|
||||||
|
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||||
|
{fields.map((field) =>
|
||||||
|
field.type === "text" ? (
|
||||||
|
<TextInput key={field.name} label={field.label} {...form.getInputProps(field.name)} />
|
||||||
|
) : (
|
||||||
|
<Select
|
||||||
|
key={field.name}
|
||||||
|
label={field.label}
|
||||||
|
data={field.name === "id_fuel_type" ? fuelTypes : field.options || []} // Используем API-данные для "Вид топлива"
|
||||||
|
value={form.values[field.name] || ""} // Указываем явно значение
|
||||||
|
{...form.getInputProps(field.name)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<Button fullWidth mt="md" type="submit">
|
||||||
|
Сохранить
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DictionaryModal;
|
||||||
17
src/components/DictionaryPage.tsx
Normal file
17
src/components/DictionaryPage.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import {TextInput, Button}from '@mantine/core';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
const DictionaryPage = () => {
|
||||||
|
const [inputValue, setInputValue] = useState("")
|
||||||
|
const saveDataHandler = (value: String) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TextInput label="Введите название" value={inputValue}
|
||||||
|
onChange={(event) => setInputValue(event.currentTarget.value)}/>
|
||||||
|
<Button onClick={() => saveDataHandler(inputValue)}>Сохранить</Button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default DictionaryPage;
|
||||||
125
src/components/DictionaryTable.tsx
Normal file
125
src/components/DictionaryTable.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Table, ActionIcon, Text, Modal, Button, Pagination } from "@mantine/core";
|
||||||
|
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||||
|
import { fetchDictionary } from "../api/api";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: any[];
|
||||||
|
onEdit: (item: any) => void;
|
||||||
|
onDelete: (id: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnNames: Record<string, string> = {
|
||||||
|
name: "Наименование",
|
||||||
|
name_short: "Аббревиатура",
|
||||||
|
id_fuel_type: "Вид топлива",
|
||||||
|
};
|
||||||
|
|
||||||
|
const DictionaryTable: React.FC<Props> = ({ data, onEdit, onDelete }) => {
|
||||||
|
const [deleteModalOpened, setDeleteModalOpened] = useState(false);
|
||||||
|
const [selectedItem, setSelectedItem] = useState<any | null>(null);
|
||||||
|
const [fuelTypesMap, setFuelTypesMap] = useState<Map<string, string>>(new Map());
|
||||||
|
|
||||||
|
// Пагинация
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const itemsPerPage = 10;
|
||||||
|
|
||||||
|
// Загружаем виды топлива
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDictionary("fuel_types").then((fuelTypes) => {
|
||||||
|
const map = new Map(fuelTypes.map((fuel: any) => [String(fuel.id), fuel.name]));
|
||||||
|
setFuelTypesMap(map);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openDeleteModal = (item: any) => {
|
||||||
|
setSelectedItem(item);
|
||||||
|
setDeleteModalOpened(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (selectedItem) {
|
||||||
|
onDelete(selectedItem.id);
|
||||||
|
setDeleteModalOpened(false);
|
||||||
|
setSelectedItem(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Фильтрация данных для текущей страницы
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const paginatedData = data.slice(startIndex, startIndex + itemsPerPage);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Table striped highlightOnHover withColumnBorders>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
{data.length > 0 ? (
|
||||||
|
Object.keys(data[0]).map((key) => (
|
||||||
|
<Table.Th key={key} style={{ textAlign: "left" }}>
|
||||||
|
{columnNames[key] || key}
|
||||||
|
</Table.Th>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Table.Th></Table.Th>
|
||||||
|
)}
|
||||||
|
{data.length > 0 && <Table.Th style={{ textAlign: "left" }}>Действия</Table.Th>}
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{paginatedData.length > 0 ? (
|
||||||
|
paginatedData.map((item) => (
|
||||||
|
<Table.Tr key={item.id}>
|
||||||
|
{Object.keys(item).map((key, idx) => (
|
||||||
|
<Table.Td key={idx}>
|
||||||
|
{key === "id_fuel_type" ? fuelTypesMap.get(String(item[key])) || "Неизвестно" : String(item[key])}
|
||||||
|
</Table.Td>
|
||||||
|
))}
|
||||||
|
<Table.Td>
|
||||||
|
<ActionIcon color="blue" onClick={() => onEdit(item)} style={{ marginRight: "10px" }}>
|
||||||
|
<IconEdit />
|
||||||
|
</ActionIcon>
|
||||||
|
<ActionIcon color="red" onClick={() => openDeleteModal(item)}>
|
||||||
|
<IconTrash />
|
||||||
|
</ActionIcon>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td colSpan={data.length > 0 ? Object.keys(data[0]).length + 1 : 1} style={{ textAlign: "center" }}>
|
||||||
|
<Text c="dimmed">Нет записей</Text>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
)}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{/* Пагинация */}
|
||||||
|
{data.length > itemsPerPage && (
|
||||||
|
<Pagination
|
||||||
|
total={Math.ceil(data.length / itemsPerPage)}
|
||||||
|
value={currentPage}
|
||||||
|
onChange={setCurrentPage}
|
||||||
|
mt="md"
|
||||||
|
position="center"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Модальное окно подтверждения удаления */}
|
||||||
|
<Modal opened={deleteModalOpened} onClose={() => setDeleteModalOpened(false)} title="Подтверждение удаления" centered>
|
||||||
|
<Text>Вы уверены, что хотите удалить <b>{selectedItem?.name || "элемент"}</b>?</Text>
|
||||||
|
<div style={{ marginTop: "20px", display: "flex", justifyContent: "flex-end" }}>
|
||||||
|
<Button variant="outline" onClick={() => setDeleteModalOpened(false)} style={{ marginRight: "10px" }}>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
<Button color="red" onClick={handleDelete}>
|
||||||
|
Удалить
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DictionaryTable;
|
||||||
57
src/components/Layout.tsx
Normal file
57
src/components/Layout.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { AppShell, Burger, Group, Text, NavLink } from "@mantine/core";
|
||||||
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
import { Outlet, useNavigate, useLocation } from "react-router-dom";
|
||||||
|
import { IconHome2, IconGauge, IconActivity, IconUser } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
export function Layout() {
|
||||||
|
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
|
||||||
|
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell
|
||||||
|
header={{ height: 60 }}
|
||||||
|
navbar={{
|
||||||
|
width: 300,
|
||||||
|
breakpoint: "sm",
|
||||||
|
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
|
||||||
|
}}
|
||||||
|
padding="md"
|
||||||
|
>
|
||||||
|
<AppShell.Header>
|
||||||
|
<Group h="100%" px="md">
|
||||||
|
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
|
||||||
|
<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
|
||||||
|
<img src="/fuel.png" width={50} />
|
||||||
|
<Text size="xl" fw={900} variant="gradient" gradient={{ from: "blue", to: "cyan", deg: 90 }}>
|
||||||
|
Топливо и Транспорт
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
<AppShell.Navbar p="md">
|
||||||
|
<NavLink
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
label="Главная"
|
||||||
|
leftSection={<IconHome2 size="1rem" stroke={1.5} />}
|
||||||
|
active={location.pathname === "/"}
|
||||||
|
/>
|
||||||
|
<NavLink
|
||||||
|
onClick={() => navigate("/dictionaries")}
|
||||||
|
label="Справочники"
|
||||||
|
leftSection={<IconGauge size="1rem" stroke={1.5} />}
|
||||||
|
active={location.pathname === "/dictionaries"}
|
||||||
|
/>
|
||||||
|
<NavLink
|
||||||
|
onClick={() => navigate("/drivers")}
|
||||||
|
label="Водители"
|
||||||
|
leftSection={<IconUser size="1rem" stroke={1.5} />}
|
||||||
|
active={location.pathname === "/drivers"}
|
||||||
|
/>
|
||||||
|
</AppShell.Navbar>
|
||||||
|
<AppShell.Main>
|
||||||
|
<Outlet />
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/components/MaskedDateInput.tsx
Normal file
10
src/components/MaskedDateInput.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { InputBase } from "@mantine/core"
|
||||||
|
import { IMaskInput } from 'react-imask';
|
||||||
|
|
||||||
|
const MaskedDateInput = () => {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputBase component={IMaskInput} mask="00.00.0000" placeholder="ДД.ММ.ГГГГ"/>
|
||||||
|
)
|
||||||
|
}
|
||||||
11
src/main.tsx
Normal file
11
src/main.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import App from './App.tsx'
|
||||||
|
import '@mantine/core/styles.css';
|
||||||
|
import '@mantine/dates/styles.css';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
103
src/pages/Dictionaries.tsx
Normal file
103
src/pages/Dictionaries.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Button, Select } from "@mantine/core";
|
||||||
|
import DictionaryTable from "../components/DictionaryTable";
|
||||||
|
import DictionaryModal from "../components/DictionaryModal";
|
||||||
|
import { fetchDictionary, createDictionaryItem, updateDictionaryItem, deleteDictionaryItem } from "../api/api";
|
||||||
|
|
||||||
|
const dictionaryOptions = [
|
||||||
|
{ value: "fuel_types", label: "Вид топлива" },
|
||||||
|
{ value: "fuel", label: "Наименование топлива" },
|
||||||
|
{ value: "unit", label: "Единица измерения" },
|
||||||
|
{ value: "vehicle_class", label: "Категория транспорта" },
|
||||||
|
{ value: "vehicle_type", label: "Тип транспорта" },
|
||||||
|
{ value: "vehicle_activity", label: "Вид деятельности транспорта" },
|
||||||
|
{ value: "driver_license_category", label: "Категория водительских удостоверений" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const fieldMappings: Record<string, any[]> = {
|
||||||
|
fuel_types: [{ name: "name", label: "Наименование", type: "text" }],
|
||||||
|
fuel: [
|
||||||
|
{ name: "name", label: "Наименование", type: "text" },
|
||||||
|
{ name: "id_fuel_type", label: "Вид топлива", type: "select", options: [] },
|
||||||
|
],
|
||||||
|
unit: [
|
||||||
|
{ name: "name", label: "Наименование", type: "text" },
|
||||||
|
{ name: "name_short", label: "Аббревиатура", type: "text" },
|
||||||
|
],
|
||||||
|
vehicle_class: [{ name: "name", label: "Наименование", type: "text" }],
|
||||||
|
vehicle_type: [{ name: "name", label: "Наименование", type: "text" }],
|
||||||
|
vehicle_activity: [{ name: "name", label: "Наименование", type: "text" }],
|
||||||
|
driver_license_category: [
|
||||||
|
{ name: "name", label: "Наименование", type: "text" },
|
||||||
|
{ name: "name_short", label: "Аббревиатура", type: "text" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const Dictionaries = () => {
|
||||||
|
const [selectedDictionary, setSelectedDictionary] = useState("fuel_types");
|
||||||
|
const [data, setData] = useState<any[]>([]);
|
||||||
|
const [modalOpened, setModalOpened] = useState(false);
|
||||||
|
const [currentItem, setCurrentItem] = useState<any | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDictionary(selectedDictionary).then(setData);
|
||||||
|
}, [selectedDictionary]);
|
||||||
|
|
||||||
|
const handleAdd = async (values: any) => {
|
||||||
|
const newItem = await createDictionaryItem(selectedDictionary, values);
|
||||||
|
setData((prev) => [...prev, newItem]); // Обновляем локальное состояние
|
||||||
|
setModalOpened(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = async (values: any) => {
|
||||||
|
if (!currentItem) return;
|
||||||
|
const updatedItem = await updateDictionaryItem(selectedDictionary, currentItem.id, values);
|
||||||
|
setData((prev) => prev.map((item) => (item.id === currentItem.id ? updatedItem : item))); // Обновляем локальное состояние
|
||||||
|
setModalOpened(false);
|
||||||
|
setCurrentItem(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
await deleteDictionaryItem(selectedDictionary, id);
|
||||||
|
setData((prev) => prev.filter((item) => item.id !== id)); // Обновляем локальное состояние
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEmptyInitialValues = () => {
|
||||||
|
return fieldMappings[selectedDictionary]?.reduce((acc, field) => {
|
||||||
|
acc[field.name] = "";
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, string>);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3>Справочники</h3>
|
||||||
|
<Select
|
||||||
|
data={dictionaryOptions}
|
||||||
|
value={selectedDictionary}
|
||||||
|
onChange={setSelectedDictionary}
|
||||||
|
allowDeselect={false}
|
||||||
|
searchable
|
||||||
|
nothingFoundMessage="Не найдено..."
|
||||||
|
maxDropdownHeight={300}
|
||||||
|
/><br/>
|
||||||
|
<Button onClick={() => { setCurrentItem(null); setModalOpened(true); }}>Добавить</Button><br/><br/>
|
||||||
|
|
||||||
|
<DictionaryTable
|
||||||
|
data={data}
|
||||||
|
onEdit={(item) => { setCurrentItem(item); setModalOpened(true); }}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DictionaryModal
|
||||||
|
opened={modalOpened}
|
||||||
|
onClose={() => setModalOpened(false)}
|
||||||
|
onSubmit={currentItem ? handleEdit : handleAdd}
|
||||||
|
initialValues={currentItem || getEmptyInitialValues()}
|
||||||
|
fields={fieldMappings[selectedDictionary] || []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dictionaries;
|
||||||
259
src/pages/DriverForm.tsx
Normal file
259
src/pages/DriverForm.tsx
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
import 'dayjs/locale/ru';
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
TextInput,
|
||||||
|
Table,
|
||||||
|
ActionIcon,
|
||||||
|
Modal,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { DateInput } from "@mantine/dates";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||||
|
import {
|
||||||
|
fetchDictionary,
|
||||||
|
createDictionaryItem,
|
||||||
|
updateDictionaryItem,
|
||||||
|
deleteDictionaryItem,
|
||||||
|
} from "../api/api";
|
||||||
|
|
||||||
|
const DriverForm = () => {
|
||||||
|
const [drivers, setDrivers] = useState<any[]>([]);
|
||||||
|
const [licenseCategories, setLicenseCategories] = useState<{ value: string; label: string }[]>([]);
|
||||||
|
const [selectedDriver, setSelectedDriver] = useState<any | null>(null);
|
||||||
|
const [selectedLicense, setSelectedLicense] = useState<any | null>(null);
|
||||||
|
const [modalOpened, setModalOpened] = useState(false);
|
||||||
|
const [licenseModalOpened, setLicenseModalOpened] = useState(false);
|
||||||
|
const [dateValue, setDateValue] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDictionary("driver_license_category").then((data) => {
|
||||||
|
const options = data.map((item: any) => ({
|
||||||
|
value: String(item.id),
|
||||||
|
label: item.name_short, // Берем короткое название (например, "B", "C", "D")
|
||||||
|
}));
|
||||||
|
setLicenseCategories(options);
|
||||||
|
});
|
||||||
|
|
||||||
|
fetchDictionary("driver").then(setDrivers);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const driverForm = useForm({
|
||||||
|
initialValues: {
|
||||||
|
fullname: "Попов Спиридон Семенович",
|
||||||
|
snils: "11122233344",
|
||||||
|
birthday: null,
|
||||||
|
iin: "123456789123",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const licenseForm = useForm({
|
||||||
|
initialValues: {
|
||||||
|
driverId: "",
|
||||||
|
series_number: "",
|
||||||
|
form_date: "",
|
||||||
|
to_date: "",
|
||||||
|
categories: [],
|
||||||
|
frontPhoto: null,
|
||||||
|
backPhoto: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function dateToYYYYMMDD(date) {
|
||||||
|
if (!(date instanceof Date) || isNaN(date)) {
|
||||||
|
return ""; // Handle invalid Date objects
|
||||||
|
}
|
||||||
|
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0'); // Month is 0-indexed
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddDriver = async (values: any) => {
|
||||||
|
const date = new Date(Date.parse(values.birthday))
|
||||||
|
|
||||||
|
const newValues = {
|
||||||
|
fullname: values.fullname,
|
||||||
|
snils: values.snils,
|
||||||
|
birthday: dateToYYYYMMDD(date),
|
||||||
|
iin: values.iin
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedDriver) {
|
||||||
|
const updatedDriver = await updateDictionaryItem("driver", selectedDriver.id, newValues);
|
||||||
|
setDrivers((prev) => prev.map((d) => (d.id === selectedDriver.id ? updatedDriver : d)));
|
||||||
|
} else {
|
||||||
|
const newDriver = await createDictionaryItem("driver", newValues);
|
||||||
|
setDrivers((prev) => [...prev, newDriver]);
|
||||||
|
}
|
||||||
|
setModalOpened(false);
|
||||||
|
driverForm.reset();
|
||||||
|
setSelectedDriver(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteDriver = async (id: number) => {
|
||||||
|
await deleteDictionaryItem("driver", id);
|
||||||
|
setDrivers((prev) => prev.filter((driver) => driver.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditDriver = (driver: any) => {
|
||||||
|
driverForm.setValues(driver);
|
||||||
|
setSelectedDriver(driver);
|
||||||
|
setModalOpened(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddLicense = async (values: any) => {
|
||||||
|
if (selectedLicense) {
|
||||||
|
const updatedLicense = await updateDictionaryItem("driver_license", selectedLicense.id, values);
|
||||||
|
setDrivers((prev) =>
|
||||||
|
prev.map((driver) =>
|
||||||
|
driver.id === values.driverId
|
||||||
|
? {
|
||||||
|
...driver,
|
||||||
|
license: driver.license.map((lic) => (lic.id === selectedLicense.id ? updatedLicense : lic)),
|
||||||
|
}
|
||||||
|
: driver
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const newLicense = await createDictionaryItem("driver_license", values);
|
||||||
|
const updatedDriver = await updateDictionaryItem("driver_connection", {
|
||||||
|
driver_id: values.driverId,
|
||||||
|
driver_license_id: newLicense.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
setDrivers((prev) =>
|
||||||
|
prev.map((driver) =>
|
||||||
|
driver.id === values.driverId
|
||||||
|
? { ...driver, license: [...(driver.license || []), newLicense] }
|
||||||
|
: driver
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLicenseModalOpened(false);
|
||||||
|
licenseForm.reset();
|
||||||
|
setSelectedLicense(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteLicense = async (driverId: number, licenseId: number) => {
|
||||||
|
await deleteDictionaryItem("driver_license", licenseId);
|
||||||
|
setDrivers((prev) =>
|
||||||
|
prev.map((driver) =>
|
||||||
|
driver.id === driverId ? { ...driver, license: driver.license.filter((lic) => lic.id !== licenseId) } : driver
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseDDMMYYYY(dateString: string): Date | null {
|
||||||
|
const [day, month, year] = dateString.split('.').map(Number);
|
||||||
|
|
||||||
|
if (
|
||||||
|
isNaN(day) ||
|
||||||
|
isNaN(month) ||
|
||||||
|
isNaN(year) ||
|
||||||
|
month < 1 ||
|
||||||
|
month > 12 ||
|
||||||
|
year < 1000 || // Prevent extremely early dates
|
||||||
|
year > 9999
|
||||||
|
) {
|
||||||
|
return null; // Invalid input
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(year, month - 1, day); // Month is 0-indexed in Date
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Водители</h2>
|
||||||
|
<Button onClick={() => setModalOpened(true)}>Добавить водителя</Button>
|
||||||
|
<Table striped highlightOnHover withColumnBorders mt="md">
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>ФИО</Table.Th>
|
||||||
|
<Table.Th>СНИЛС</Table.Th>
|
||||||
|
<Table.Th>Дата рождения</Table.Th>
|
||||||
|
<Table.Th>ИНН</Table.Th>
|
||||||
|
<Table.Th>Действия</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{drivers.map((driver) => (
|
||||||
|
<Table.Tr key={driver.id}>
|
||||||
|
<Table.Td>{driver.fullname}</Table.Td>
|
||||||
|
<Table.Td>{driver.snils}</Table.Td>
|
||||||
|
<Table.Td>{driver.birthday}</Table.Td>
|
||||||
|
<Table.Td>{driver.iin}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<ActionIcon color="blue" onClick={() => handleEditDriver(driver)} style={{ marginRight: "10px" }}>
|
||||||
|
<IconEdit />
|
||||||
|
</ActionIcon>
|
||||||
|
<ActionIcon color="red" onClick={() => handleDeleteDriver(driver.id)}>
|
||||||
|
<IconTrash />
|
||||||
|
</ActionIcon>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<h2>Водительские права</h2>
|
||||||
|
<Button onClick={() => setLicenseModalOpened(true)}>Добавить права</Button>
|
||||||
|
<Table striped highlightOnHover withColumnBorders mt="md">
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>ФИО</Table.Th>
|
||||||
|
<Table.Th>Серия и номер</Table.Th>
|
||||||
|
<Table.Th>Дата выдачи</Table.Th>
|
||||||
|
<Table.Th>Срок действия</Table.Th>
|
||||||
|
<Table.Th>Категории</Table.Th>
|
||||||
|
<Table.Th>Действия</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{drivers.flatMap((driver) =>
|
||||||
|
driver.license?.map((license) => (
|
||||||
|
<Table.Tr key={license.id}>
|
||||||
|
<Table.Td>{driver.fullname}</Table.Td>
|
||||||
|
<Table.Td>{license.series_number}</Table.Td>
|
||||||
|
<Table.Td>{license.form_date}</Table.Td>
|
||||||
|
<Table.Td>{license.to_date}</Table.Td>
|
||||||
|
<Table.Td>{license.categories?.map((c) => c.name_short).join(", ")}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<ActionIcon color="red" onClick={() => handleDeleteLicense(driver.id, license.id)}>
|
||||||
|
<IconTrash />
|
||||||
|
</ActionIcon>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<Modal opened={modalOpened} onClose={() => setModalOpened(false)} title="Добавить/Редактировать водителя">
|
||||||
|
<form onSubmit={driverForm.onSubmit(handleAddDriver)}>
|
||||||
|
<TextInput label="ФИО" {...driverForm.getInputProps("fullname")} required />
|
||||||
|
<TextInput label="СНИЛС" {...driverForm.getInputProps("snils")} required />
|
||||||
|
<TextInput label="ИНН" {...driverForm.getInputProps("iin")} required />
|
||||||
|
<DateInput
|
||||||
|
label="Дата рождения"
|
||||||
|
placeholder="ДД.MM.ГГГГ"
|
||||||
|
defaultLevel='month'
|
||||||
|
valueFormat='DD.MM.YYYY'
|
||||||
|
required
|
||||||
|
dateParser={parseDDMMYYYY}
|
||||||
|
locale="ru"
|
||||||
|
{...driverForm.getInputProps("birthday")}
|
||||||
|
/>
|
||||||
|
<Button fullWidth mt="md" type="submit">
|
||||||
|
Сохранить
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DriverForm;
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
26
tsconfig.app.json
Normal file
26
tsconfig.app.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
24
tsconfig.node.json
Normal file
24
tsconfig.node.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user