From 3d3d6504c2df3a188aa426ca03b77ca72b5e906c Mon Sep 17 00:00:00 2001 From: lluch Date: Fri, 28 Nov 2025 23:39:01 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20+=20=D0=B6=D1=83=D1=80=D0=BD=D0=B0?= =?UTF-8?q?=D0=BB=20=D1=83=D1=81=D0=BF=D0=B5=D0=B2=D0=B0=D0=B5=D0=BC=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/package-lock.json | 17 +- web/src/App/AppRouter.jsx | 2 + .../Pages/GradebookPage/GradebookPage.jsx | 723 ++++++++++++++++++ .../Components/Pages/LoginPage/LoginPage.jsx | 59 +- .../Pages/LoginPage/useLoginPage.js | 28 +- .../Pages/RegisterPage/RegisterPage.jsx | 146 ++++ .../Pages/RegisterPage/useRegisterPage.js | 27 + 7 files changed, 971 insertions(+), 31 deletions(-) create mode 100644 web/src/Components/Pages/GradebookPage/GradebookPage.jsx create mode 100644 web/src/Components/Pages/RegisterPage/RegisterPage.jsx create mode 100644 web/src/Components/Pages/RegisterPage/useRegisterPage.js diff --git a/web/package-lock.json b/web/package-lock.json index 6d5f6a3..7a0ccda 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -153,6 +153,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2224,6 +2225,7 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2271,6 +2273,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2439,6 +2442,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -2594,7 +2598,8 @@ "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -2699,6 +2704,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3410,6 +3416,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3536,6 +3543,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3545,6 +3553,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3563,6 +3572,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -3633,7 +3643,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -3905,6 +3916,7 @@ "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -4026,6 +4038,7 @@ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/web/src/App/AppRouter.jsx b/web/src/App/AppRouter.jsx index e4ca10f..f063a10 100644 --- a/web/src/App/AppRouter.jsx +++ b/web/src/App/AppRouter.jsx @@ -2,6 +2,7 @@ import {Routes, Route, Navigate} from "react-router-dom"; import PrivateRoute from "./PrivateRoute.jsx"; import AdminRoute from "./AdminRoute.jsx"; import LoginPage from "../Components/Pages/LoginPage/LoginPage.jsx"; +import RegisterPage from "../Components/Pages/RegisterPage/RegisterPage.jsx"; // Новая страница регистрации import CoursesPage from "../Components/Pages/Courses/CoursesPage.jsx"; import MainLayout from "../Components/Layouts/MainLayout.jsx"; import ProfilePage from "../Components/Pages/ProfilePage/ProfilePage.jsx"; @@ -11,6 +12,7 @@ import AdminPage from "../Components/Pages/AdminPage/AdminPage.jsx"; const AppRouter = () => ( }/> + }/> }> }> diff --git a/web/src/Components/Pages/GradebookPage/GradebookPage.jsx b/web/src/Components/Pages/GradebookPage/GradebookPage.jsx new file mode 100644 index 0000000..c4dd14c --- /dev/null +++ b/web/src/Components/Pages/GradebookPage/GradebookPage.jsx @@ -0,0 +1,723 @@ +import { useState, useMemo } from "react"; +import { Avatar, Progress, Tag, Tooltip, Space, Button, Table, Card, Row, Col, Statistic, Select, Typography, Input, Modal, Badge } from "antd"; +import { UserOutlined, SearchOutlined, SortAscendingOutlined, SortDescendingOutlined, FilterOutlined } from "@ant-design/icons"; + +const { Title } = Typography; +const { Search } = Input; + +const GradebookPage = ({ onLogout }) => { + const [selectedCourse, setSelectedCourse] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [filters, setFilters] = useState({ + groups: [], + searchText: "", + progressRange: [0, 100] + }); + const [sortConfig, setSortConfig] = useState({ + key: null, + direction: 'ascend' + }); + const [isSearchModalVisible, setIsSearchModalVisible] = useState(false); + + // Заглушки для данных + const courses = [ + { id: 1, title: "Основы программирования" }, + { id: 2, title: "Веб-разработка" }, + { id: 3, title: "Базы данных" }, + { id: 4, title: "Алгоритмы и структуры данных" }, + ]; + + const assignments = [ + { id: 1, name: "ДЗ №1", maxScore: 100 }, + { id: 2, name: "ДЗ №2", maxScore: 100 }, + { id: 3, name: "ДЗ №3", maxScore: 100 }, + { id: 4, name: "Проект", maxScore: 200 }, + ]; + + const students = [ + { + id: 1, + firstName: "Иван", + lastName: "Иванов", + email: "ivanov@mail.com", + group: "ПИ-201", + grades: { 1: 85, 2: 92, 3: 78, 4: 180 }, + progress: 85 + }, + { + id: 2, + firstName: "Мария", + lastName: "Петрова", + email: "petrova@mail.com", + group: "ПИ-201", + grades: { 1: 95, 2: 88, 3: 91, 4: 190 }, + progress: 92 + }, + { + id: 3, + firstName: "Алексей", + lastName: "Сидоров", + email: "sidorov@mail.com", + group: "ПИ-202", + grades: { 1: 72, 2: null, 3: 85, 4: null }, + progress: 52 + }, + { + id: 4, + firstName: "Екатерина", + lastName: "Смирнова", + email: "smirnova@mail.com", + group: "ПИ-202", + grades: { 1: 88, 2: 90, 3: 87, 4: 175 }, + progress: 88 + }, + { + id: 5, + firstName: "Дмитрий", + lastName: "Козлов", + email: "kozlov@mail.com", + group: "ПИ-203", + grades: { 1: 65, 2: 70, 3: null, 4: null }, + progress: 45 + }, + { + id: 6, + firstName: "Анна", + lastName: "Волкова", + email: "volkova@mail.com", + group: "ПИ-201", + grades: { 1: 100, 2: 98, 3: 95, 4: 195 }, + progress: 97 + }, + { + id: 7, + firstName: "Сергей", + lastName: "Орлов", + email: "orlov@mail.com", + group: "ПИ-203", + grades: { 1: 80, 2: 85, 3: 82, 4: 170 }, + progress: 79 + }, + { + id: 8, + firstName: "Ольга", + lastName: "Лебедева", + email: "lebedeva@mail.com", + group: "ПИ-202", + grades: { 1: 90, 2: 92, 3: 88, 4: 185 }, + progress: 89 + }, + ]; + + // Получение уникальных групп для фильтра + const uniqueGroups = useMemo(() => { + return [...new Set(students.map(student => student.group))]; + }, [students]); + + // Фильтрация и сортировка данных + const filteredAndSortedStudents = useMemo(() => { + let filtered = students.filter(student => { + // Фильтр по группам + if (filters.groups.length > 0 && !filters.groups.includes(student.group)) { + return false; + } + + // Поиск по тексту (ФИО, email, группа) + if (filters.searchText) { + const searchLower = filters.searchText.toLowerCase(); + const fullName = `${student.firstName} ${student.lastName}`.toLowerCase(); + if (!fullName.includes(searchLower) && + !student.email.toLowerCase().includes(searchLower) && + !student.group.toLowerCase().includes(searchLower)) { + return false; + } + } + + // Фильтр по прогрессу + if (student.progress < filters.progressRange[0] || student.progress > filters.progressRange[1]) { + return false; + } + + return true; + }); + + // Сортировка + if (sortConfig.key) { + filtered.sort((a, b) => { + let aValue, bValue; + + switch (sortConfig.key) { + case 'student': + aValue = `${a.firstName} ${a.lastName}`; + bValue = `${b.firstName} ${b.lastName}`; + break; + case 'group': + aValue = a.group; + bValue = b.group; + break; + case 'progress': + aValue = a.progress; + bValue = b.progress; + break; + case 'average': + const aGrades = Object.values(a.grades).filter(g => g !== null); + const bGrades = Object.values(b.grades).filter(g => g !== null); + aValue = aGrades.length > 0 ? aGrades.reduce((sum, grade) => sum + grade, 0) / aGrades.length : 0; + bValue = bGrades.length > 0 ? bGrades.reduce((sum, grade) => sum + grade, 0) / bGrades.length : 0; + break; + default: + // Сортировка по заданиям + if (sortConfig.key.startsWith('assignment_')) { + const assignmentId = parseInt(sortConfig.key.split('_')[1]); + aValue = a.grades[assignmentId] || 0; + bValue = b.grades[assignmentId] || 0; + } else { + aValue = a[sortConfig.key]; + bValue = b[sortConfig.key]; + } + } + + if (aValue < bValue) { + return sortConfig.direction === 'ascend' ? -1 : 1; + } + if (aValue > bValue) { + return sortConfig.direction === 'ascend' ? 1 : -1; + } + return 0; + }); + } + + return filtered; + }, [students, filters, sortConfig]); + + // Обработчики фильтров + const handleGroupFilter = (selectedGroups) => { + setFilters(prev => ({ ...prev, groups: selectedGroups })); + }; + + const handleSearch = (value) => { + setFilters(prev => ({ ...prev, searchText: value })); + setIsSearchModalVisible(false); + }; + + const handleSort = (key) => { + setSortConfig(prev => ({ + key, + direction: prev.key === key && prev.direction === 'ascend' ? 'descend' : 'ascend' + })); + }; + + // Колонки таблицы с фильтрацией и сортировкой + const columns = useMemo(() => { + const baseColumns = [ + { + title: ( + + Студент + , + , + ]} + width={400} + > + +
+

+ Поиск по ФИО, email или группе: +

+ +
+ {filters.searchText && ( +
+ + Активный поиск: "{filters.searchText}" + +
+ )} +
+ + ); + + return ( +
+ {/* Шапка с навигацией */} + + + Электронный журнал + + + + {/* Выбор курса */} + + + Выберите курс: + + - - + + - + + + Нет аккаунта? + + + + - ) + ); }; -export default LoginPage; \ No newline at end of file +export default LoginPage; diff --git a/web/src/Components/Pages/LoginPage/useLoginPage.js b/web/src/Components/Pages/LoginPage/useLoginPage.js index 1549f6e..455e734 100644 --- a/web/src/Components/Pages/LoginPage/useLoginPage.js +++ b/web/src/Components/Pages/LoginPage/useLoginPage.js @@ -1,12 +1,12 @@ -import {useNavigate} from "react-router-dom"; -import {useDispatch, useSelector} from "react-redux"; -import {useLoginMutation} from "../../../Api/authApi.js"; -import {useEffect, useRef} from "react"; -import {notification} from "antd"; -import {checkAuth, setError, setUser} from "../../../Redux/Slices/authSlice.js"; +import { useNavigate } from "react-router-dom"; +import { useDispatch, useSelector } from "react-redux"; +import { useLoginMutation } from "../../../Api/authApi.js"; +import { useEffect, useRef } from "react"; +import { notification } from "antd"; +import { checkAuth, setError, setUser } from "../../../Redux/Slices/authSlice.js"; +import { message } from "antd"; - -const useLoginPage = () => { +const LoginPage = () => { const navigate = useNavigate(); const dispatch = useDispatch(); const [loginUser, { isLoading }] = useLoginMutation(); @@ -14,7 +14,11 @@ const useLoginPage = () => { const hasRedirected = useRef(false); const pageContainerStyle = { - paddingTop: screen.xs ? "100px" : "200px", + width: 400, + padding: 24, + borderRadius: 8, + boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)", + backgroundColor: "white", }; useEffect(() => { @@ -26,6 +30,7 @@ const useLoginPage = () => { }, [user, userData, isLoading, navigate]); const onFinish = async (loginData) => { + // РЕАЛЬНАЯ АВТОРИЗАЦИЯ try { const response = await loginUser(loginData).unwrap(); const token = response.access_token || response.token; @@ -50,8 +55,9 @@ const useLoginPage = () => { return { pageContainerStyle, - onFinish + onFinish, + isLoading }; }; -export default useLoginPage; \ No newline at end of file +export default LoginPage; \ No newline at end of file diff --git a/web/src/Components/Pages/RegisterPage/RegisterPage.jsx b/web/src/Components/Pages/RegisterPage/RegisterPage.jsx new file mode 100644 index 0000000..0c0e7b3 --- /dev/null +++ b/web/src/Components/Pages/RegisterPage/RegisterPage.jsx @@ -0,0 +1,146 @@ +import {Button, Col, Flex, Form, Input, Select, Typography} from "antd"; +import {UserOutlined, MailOutlined, LockOutlined} from "@ant-design/icons"; +import useRegisterPage from "./useRegisterPage.js"; + +const {Title, Text} = Typography; + +const RegisterPage = () => { + const { + pageContainerStyle, + onFinish + } = useRegisterPage(); + + return ( + + + Регистрация + + Создайте новый аккаунт для работы с платформой + + +
+ + + + + + } + placeholder="Иван" + size="large" + /> + + + + } + placeholder="Иванов" + size="large" + /> + + + + } + placeholder="example@mail.com" + size="large" + /> + + + + } + placeholder="Логин" + size="large" + /> + + + + } + placeholder="Пароль" + size="large" + /> + + + ({ + validator(_, value) { + if (!value || getFieldValue('password') === value) { + return Promise.resolve(); + } + return Promise.reject(new Error('Пароли не совпадают')); + }, + }), + ]} + > + } + placeholder="Подтвердите пароль" + size="large" + /> + + + + + + + + + Уже есть аккаунт? Войти + + +
+ +
+ ); +}; + +export default RegisterPage; \ No newline at end of file diff --git a/web/src/Components/Pages/RegisterPage/useRegisterPage.js b/web/src/Components/Pages/RegisterPage/useRegisterPage.js new file mode 100644 index 0000000..11bb796 --- /dev/null +++ b/web/src/Components/Pages/RegisterPage/useRegisterPage.js @@ -0,0 +1,27 @@ +import { message } from 'antd'; +import { useNavigate } from 'react-router-dom'; + +const useRegisterPage = () => { + const navigate = useNavigate(); + + const pageContainerStyle = { + width: 450, + padding: 40, + borderRadius: 12, + boxShadow: "0 8px 24px rgba(0, 0, 0, 0.1)", + backgroundColor: "white", + }; + + const onFinish = (values) => { + console.log('Регистрация:', values); + message.success('Регистрация выполнена успешно!'); + navigate('/login'); + }; + + return { + pageContainerStyle, + onFinish + }; +}; + +export default useRegisterPage; \ No newline at end of file