diff --git a/.env.development b/.env.development
new file mode 100644
index 0000000..68bb9f3
--- /dev/null
+++ b/.env.development
@@ -0,0 +1,2 @@
+VITE_AUTH_API_BASE_URL=https://api.test.profcomff.com
+VITE_TVOI_FF_TOKEN=null
\ No newline at end of file
diff --git a/README.md b/README.md
index 46df55a..343642a 100644
--- a/README.md
+++ b/README.md
@@ -1,111 +1,94 @@
-# Мини-приложение рейтинга преподавателей
+# Rating UI -- интерфейс сервиса оценки преподавателей "Дубинушка"
-[
](https://easycode.profcomff.com/templates/docker-node/workspace?mode=manual¶m.Repository+URL=https://github.com/profcomff/rating-ui.git¶m.Working+directory=rating-ui)
+Фронтенд для приложения по оценке преподавателей на Vue.js с TypeScript и Vite.
-В этом репозитории представлен пример простейшего фронтенд-сервиса с Vue.js.
+Большая часть команд и инструкций написаны для Linux и MacOS, но должны работать и на Windows.
-
-*Большая часть команд и инструкций написаны для операционных систем Linux и MacOS.*
+---
## Зависимости
-- Node.js v18 – среда исполнения на языке JavaScript/TypeScript
-- NPM – консольный менеджер пакетов для установки библиотек (идет в поставке с Node.js)
-- Vue.js – фраемворк для разработки фронтенда
-- Vite – консольный менеджер для удобства работы с Vue.js
-## Frontend разработка на Vue.js
+- **Node.js** >= v18 – среда выполнения JavaScript/TypeScript
+- **PNPM** – менеджер пакетов (можно использовать NPM, но мы рекомендуем PNPM)
+- **Vue.js** – фреймворк для фронтенда
+- **Vite** – инструмент сборки и локального сервера
+- **Docker** – для сборки и деплоя
-Фронтенд (англ. front end, frontend) — презентационная часть web приложения, её пользовательский
-интерфейс и связанные с ним компоненты.
+---
-В данном примере используется популярный фраемворк [Vue.js](https://vuejs.org/). Разработка ведется
-на языке TypeScript.
+## Разработка
+1. Склонируй проект:
+ 1. `cd /path/to/folder`
+ 2. `git clone https://github.com/profcomff/rental-ui.git project-folder-name`
-## Разработка
+2. Установи зависимости: `pnpm install`
-Для удобства разработки в VS Code создан [workspace](../frontend.code-workspace) с преднастроенными
-командами и рекомендованными расширениями для работы.
+3. Запусти локальный dev-сервер: `pnpm dev`
-Перед началом работы нужно установить зависимости командой
-```
-pnpm install
-```
+Приложение будет доступно на http://localhost:5173.
-Для локального запуска необходимо выполнить команду
-```
-pnpm dev
-```
+### Форматирование
+1. Открыть файл с расширением `.vue`.
+2. Открыть палитру команд (Help > Show All Commands или `Ctrl+Shift+P`)
+3. Ввести и выбрать `Format document with`.
+4. Выбрать `Configure Default Formatter`.
+5. Выбрать `Prettier`.
-### Важные замечания по коду
-- Приложение предполагает, что вы запускаете его из Твой ФФ. Чтобы имитировать запуск из Твой ФФ:
+Теперь можно форматировать файлы с помощью `Shift+Alt+F`. Еще можно настроить автоформатирование при сохранении файла (`File > Preferences > Settings`, Format on save).
- 1. Зарегистрируйтесь в тестовой среде «Твой ФФ!» по адресу https://app.test.profcomff.com/auth. Подтвердите аккаунт и войдите в пользователя (при необходимости).
+### Локальная авторизация
- 2. Перейдите в панель администрирования https://app.test.profcomff.com/admin.
+Для разработки с определенными скоупами можно проделать следующие шаги:
- 3. Нажмите кнопку «скопировать параметры приложения».
+1. Переходим в [тестовое приложение](https://app.test.profcomff.com/admin) и копируем токен.
+2. Вставляем токен в .env.development
+3. В `/src/store/profileStore.ts` в функции `setupDevAdminSession` проверяем, есть ли все нужные нам скоупы.
+4. В `src/App.vue` в хуке `onMounted`:
- 4. Подставьте полученную строку после адреса вашего приложения в браузере
+ ```typescript
+ if (import.meta.env.MODE === 'development') await profileStore.setupDevAdminSession(null);
+ ```
- Код, который обрабатывает данные пользователя из URL находится здесь: https://github.com/profcomff/app-template/blob/1070d4370d37529702d7499baeaf145ba4cd9e62/frontend/src/store/profileStore.ts#L15-L28
+> ⚠️ Важно: токен в .env.development не коммитим — он только для локального девелопмента.
+Метод создает сессию через API с заданными скоупами. Для создания сессий с другим набором скоупов можно писать аналогичные методы.
-- `./src/api/user/AuthApi.ts` и `./src/api/user/UserdataApi.ts`
+### Интеграция с "Твой ФФ"
- в этих файлах хранится код взаимодействия с [Auth API](https://api.profcomff.com/?urls.primaryName=auth)
- и [Userdata API](https://api.profcomff.com/?urls.primaryName=userdata), позволяющие получить
- информацию о текущем пользователе.
+Когда приложение подключено к Твой ФФ, оно ожидает увидеть токен, скоупы и всю дополнительную информацию в параметрах URL. Для этого в `src/store/profileStore.ts` есть метод `fromUrl` -- его стоит вызывать всегда кроме режима разработки.
-- По умолчанию используется тестовая среда для общения с API Твой ФФ! Данное поведение меняется в файле `.env`: https://github.com/profcomff/app-template/blob/main/frontend/.env
+### Пулл реквесты
+Перед пулл-реквестом:
-## Инструкции
-### Получение кода на свой компьютер
-Для работы с данным примером необходимо забрать его к себе на ПК. Для этого нужно:
-1. *(Опционально)* Если вы хотите далее опубликовать код на GitHub полезно сначала скопировать
- репозиторий к себе кнопкой Fork на GitHub. Кнопка доступна в правом верхнем углу
- [страницы репозитория](https://github.com/profcomff/app-template).
-2. Создайте папку в удобном вам расположении.
- - *Удобно создать папку на рабочем столе с названием вашего приложения.*
-3. Откройте приложение "Командная строка" или "Терминал", в зависимости от операционной системы.
-4. Прейдите в папку проекта командой `cd /путь/к/папке`.
- - Если вы пользователь windows и создали на рабочем столе папку `my_app`, то команда будет
- выглядеть так: `cd %userprofile%/Desktop/my_app`
- - Если вы пользователь linux или MacOS и создали на рабочем столе папку `my_app`, то команда
- будет выглядеть сделующим образом: `cd ~/Desktop/my_app`
-5. Склонируйте код репозитория к себе на ПК командой
- `git clone https://github.com/profcomff/app-template.git .` (точка в конце означает скачивание
- кода в текущую папку). Если вы выполнили пункт 1 используйте в команде ссылку из зеленой кнопки
- "Code" в правом верхнем углу вашего репозитория.
-6. Откройте код в удобной среде разработки. Рекомендуем использовать VSCode, который можно открыть
- из терминала командой `code .`
+1. Проверь стили: `pnpm check`
+2. _Опционально_: подробная версия `pnpm check:hard`
+3. Заполни шаблон пулл-реквеста: что, как и зачем сделано.
+4. Запроси ревью у команды.
-### Сборка и запуск приложения для публикации
-Сборка исходного кода в один пакет производится с помощью Docker. В этом случае создается
-независимый от операционной системы пакет, который можно без проблем разместить на любом сервере.
+## Сборка и деплой
-Выполните команду `make` для сборки приложения. После окончания выполнения этой команды будет создан
-новый Docker образ с названием `my_app`, который можно запустить командой `make run`
+### Сборка и запуск через Docker:
+```bash
+make # создаёт Docker образ
+make run # запускает приложение
+```
-### Автосборка
-Коммит в main запускает автоматическую сборку проекта средствами GitHub Actions. Настройки автосборки находятся в
-файле [.github/workflows/build_and_publish.yml](.github/workflows/build_and_publish.yml).
+Dockerfile и docker-compose уже настроены для простого деплоя. В .env указываем базовый URL API:
-### Тесты на Pull Request
-При создании запроса на слияние, автоматически создаются проверки кода юниттестами и на стили.
-Следующие тесты будут запущены:
-- Проверки стилей `eslint`, `prettier`, `stylelint` на код в папке `frontend`
+```env
+VITE_API_BASE_URL=https://api.example.com
+```
-Настройки автотестов находятся в файле[.github/workflows/checks.yml](.github/workflows/checks.yml).
+### CI/CD
-### Публикация готового приложения
-На данном этапе вам необходимо разместить приложение на каком-либо хостинге, поддерживающем протокол https.
+GitHub Actions автоматически проверяет код на PR: eslint, prettier, stylelint
-Для этого подойдет любой VPS сервер, который вы можете найти в интернете. Мы рекомендуем использовать VPS сервера на операционной системе linux, для удобства размещения приложений и компонентов использовать Docker.
+Автосборка: коммит в main создаёт Docker-образ через workflow .github/workflows/build_and_publish.yml
-Чтобы получить SSL сертификаы для поддержки https можно использовать letsencrypt.com, предоставляющий SSL сертификаты бесплатно. Для удобства работы можно использовать веб сервер с встроенной поддержкой этих сертификатов, например Traefik или Caddy.
+### Ссылки
-Шаблон приложения имеет уже готовые [Dockerfile](cicd/Dockerfile) для сборки вашего приложения, файл [docker-compose.production.yml](cicd/docker-compose.production.yml) с настройками развертывания приложения с Caddy Server и включенным https, а также базой данных PostgreSQL.
+API документация -- [Swagger](https://api.test.profcomff.com/?urls.primaryName=rental)
\ No newline at end of file
diff --git a/package.json b/package.json
index 2b048cb..a194f24 100644
--- a/package.json
+++ b/package.json
@@ -8,11 +8,13 @@
"docker-build": "vue-tsc && vite build --base=/ui",
"preview": "vite preview",
"lint": "eslint src/",
+ "lint:hard": "pnpm lint && pnpm lint:deadcode && pnpm lint:circular",
"lint:deadcode": "knip --exclude binaries,dependencies,unlisted",
"lint:circular": "dpdm --exit-code circular:1 --no-tree --no-warning --progress false --transform ./src/main.ts",
"format": "eslint src/ --fix && prettier src/ --write",
"stylelint": "stylelint 'src/**/*.{vue,css}' --fix",
- "check": "vue-tsc && pnpm run format && pnpm run lint && pnpm run stylelint",
+ "check": "vue-tsc && pnpm lint && pnpm format && pnpm stylelint",
+ "check:hard": "vue-tsc && pnpm lint:hard && pnpm format && pnpm stylelint",
"test": "echo ok"
},
"dependencies": {
@@ -25,7 +27,7 @@
"@eslint/eslintrc": "^3.3.0",
"@eslint/js": "^9.22.0",
"@mdi/font": "^7.4.47",
- "@profcomff/api-uilib": "^2025.10.15",
+ "@profcomff/api-uilib": "^2025.11.1-test",
"@types/node": "^22.13.10",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
@@ -56,4 +58,4 @@
"vue-tsc": "^2.2.8",
"vuetify": "^3.7.16"
}
-}
+}
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8e1370c..8282f77 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -31,8 +31,8 @@ importers:
specifier: ^7.4.47
version: 7.4.47
'@profcomff/api-uilib':
- specifier: ^2025.10.15
- version: 2025.10.15
+ specifier: ^2025.11.1-test
+ version: 2025.11.1-test
'@types/node':
specifier: ^22.13.10
version: 22.13.10
@@ -649,8 +649,8 @@ packages:
resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
- '@profcomff/api-uilib@2025.10.15':
- resolution: {integrity: sha512-M4/PlFfMR85bBNXyY3ejTaHlvcLc0Y5Oq4PQUoQm+oYW/zEOLJrAb2UjGOw9ZUUEcQTRpW1rs/2LCqeMpGGZVg==}
+ '@profcomff/api-uilib@2025.11.1-test':
+ resolution: {integrity: sha512-vNQOQABilVf06vJFivzBQB8SAfjA96yqzRFgJXBwUlTdgjCBn9A5EwMbExtQl2jpSFDaXK/QM04iKGULRufrxw==}
'@redocly/ajv@8.11.2':
resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
@@ -2872,7 +2872,7 @@ snapshots:
'@pkgr/core@0.1.1': {}
- '@profcomff/api-uilib@2025.10.15':
+ '@profcomff/api-uilib@2025.11.1-test':
dependencies:
openapi-fetch: 0.10.6
diff --git a/src/App.vue b/src/App.vue
index 0cf026f..7862383 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -8,9 +8,10 @@ import { ToastType } from './models';
const profileStore = useProfileStore();
const toastStore = useToastStore();
-onMounted(() => {
+onMounted(async () => {
profileStore.clearLocalStorage();
profileStore.fromUrl();
+ if (import.meta.env.MODE === 'development') await profileStore.setupDevAdminSession(null);
if (!profileStore.isLoggedIn) {
toastStore.push({
title: 'Не получится оставить отзыв',
diff --git a/src/components/TheLecturerSearchTable.vue b/src/components/TheLecturerSearchTable.vue
index 726234c..8918334 100644
--- a/src/components/TheLecturerSearchTable.vue
+++ b/src/components/TheLecturerSearchTable.vue
@@ -43,9 +43,9 @@
{{ item.raw.comments?.length || '—' }}
-
-
- {{ formatMark(item.raw.mark_general) }}
+
+
+ {{ formatMark(item.raw.mark_weighted) }}
@@ -80,7 +80,7 @@ const headers: CustomDataTableHeader[] = [
{ title: 'ФИО', key: 'fullName', sortable: false },
{ title: 'Предметы', key: 'subjects', sortable: false },
{ title: 'Отзывы', key: 'comments', align: 'center', sortable: false },
- { title: 'Оценка', key: 'mark_general', align: 'center', sortable: false },
+ { title: 'Оценка', key: 'mark_weighted', align: 'center', sortable: false },
];
const tableItems = computed(() => {
diff --git a/src/components/TheSearchBar.vue b/src/components/TheSearchBar.vue
index bb7506f..9bb7896 100644
--- a/src/components/TheSearchBar.vue
+++ b/src/components/TheSearchBar.vue
@@ -147,7 +147,6 @@ function changeAscDesc() {
emits('changed-asc-desc');
}
-
async function shareSearch() {
/* eslint-disable @typescript-eslint/no-explicit-any */
const params: Record = {};
diff --git a/src/pages/AdminPage.vue b/src/pages/AdminPage.vue
index 7559de0..4882596 100644
--- a/src/pages/AdminPage.vue
+++ b/src/pages/AdminPage.vue
@@ -1,38 +1,38 @@
-
-
-
-
-
-
-
-
-
-
-
-
- Нет комментариев
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ Нет комментариев
+
+
diff --git a/src/pages/LecturerPage.vue b/src/pages/LecturerPage.vue
index cbc4e47..38aa525 100644
--- a/src/pages/LecturerPage.vue
+++ b/src/pages/LecturerPage.vue
@@ -40,9 +40,9 @@ async function loadLecturer() {
return res.data;
}
-const howKind = lecturer?.mark_kindness ?? 0;
-const howFree = lecturer?.mark_freebie ?? 0;
-const howClear = lecturer?.mark_clarity ?? 0;
+const howKind = lecturer?.mark_kindness_weighted ?? 0;
+const howFree = lecturer?.mark_freebie_weighted ?? 0;
+const howClear = lecturer?.mark_clarity_weighted ?? 0;
const lecturerPhoto = getPhoto(avatarLink.value);
@@ -99,7 +99,7 @@ async function shareLecturerPage() {
- {{ lecturer?.mark_general?.toFixed(2) ?? '—' }}
+ {{ lecturer?.mark_weighted?.toFixed(2) ?? '—' }}
{{ lecturer?.comments?.length ?? 'нет' }}
{{ adaptNumeral(lecturer?.comments?.length, 'отзыв', 'отзыва', 'отзывов') }} {
- const token = ref(undefined);
- const groups = ref(null);
- const sessionScopes = ref(null);
- const isLoggedIn = ref(false);
-
- const full_name = ref(null);
-
- const fromUrl = () => {
- const url = new URL(document.location.toString());
-
- const urlToken = url.searchParams.get('token');
- const urlScopes = url.searchParams.get('scopes')?.split(',');
-
- if (urlToken === null) {
- token.value = undefined;
- isLoggedIn.value = false;
- sessionScopes.value = [];
- } else {
- token.value = urlToken;
- isLoggedIn.value = true;
- sessionScopes.value = urlScopes ?? [];
- }
-
- setupAuth(token.value);
- };
-
- const isAdmin = () => {
- return (
- (sessionScopes.value?.includes('rating.comment.review') &&
- sessionScopes.value?.includes('rating.comment.review')) ??
- false
- );
- };
-
- const clearLocalStorage = () => {
- LocalStorage.delete(LocalStorageItem.Token, LocalStorageItem.TokenScopes, LocalStorageItem.UserId);
- };
-
- return {
- token,
- groups,
- isLoggedIn,
-
- full_name,
-
- fromUrl,
- isAdmin,
- clearLocalStorage,
- };
-});
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+import { LocalStorage, LocalStorageItem } from '../models/LocalStorage';
+import { setupAuth } from '@profcomff/api-uilib';
+import apiClient from '@/api';
+
+export const useProfileStore = defineStore('profile', () => {
+ const token = ref(undefined);
+ const groups = ref(null);
+ const sessionScopes = ref(null);
+ const isLoggedIn = ref(false);
+
+ const full_name = ref(null);
+
+ const fromUrl = () => {
+ const url = new URL(document.location.toString());
+
+ const urlToken = url.searchParams.get('token');
+ const urlScopes = url.searchParams.get('scopes')?.split(',');
+
+ if (urlToken === null) {
+ token.value = undefined;
+ isLoggedIn.value = false;
+ sessionScopes.value = [];
+ } else {
+ token.value = urlToken;
+ isLoggedIn.value = true;
+ sessionScopes.value = urlScopes ?? [];
+ }
+
+ setupAuth(token.value);
+ };
+
+ const TVOI_FF_TEST_TOKEN = import.meta.env.VITE_TVOI_FF_TOKEN;
+
+ async function setupDevAdminSession(tvff_token: string | null) {
+ setupAuth(tvff_token ?? TVOI_FF_TEST_TOKEN);
+
+ const serviceScopes = [
+ 'comment.create',
+ 'comment.delete',
+ 'comment.import',
+ 'comment.review',
+ 'lecturer.comment.delete',
+ 'lecturer.comment.review',
+ ];
+ const serviceName = 'rating';
+ const scopes = serviceScopes.map(value => `${serviceName}.${value}`);
+
+ const { data, error } = await apiClient.POST('/auth/session', {
+ body: {
+ scopes,
+ expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
+ },
+ });
+
+ if (error) {
+ console.log('Ошибка при попытке авторизоваться');
+ isLoggedIn.value = false;
+ return;
+ }
+ token.value = data.token || '';
+ sessionScopes.value = data.session_scopes ?? [];
+
+ setupAuth(data.token || '');
+ isLoggedIn.value = true;
+ console.log(token);
+ }
+
+ async function setupDevUserSession(tvff_token: string | null) {
+ setupAuth(tvff_token ?? TVOI_FF_TEST_TOKEN);
+ }
+
+ const isAdmin = () => {
+ return sessionScopes.value?.includes('rating.comment.review') ?? false;
+ };
+
+ const clearLocalStorage = () => {
+ LocalStorage.delete(LocalStorageItem.Token, LocalStorageItem.TokenScopes, LocalStorageItem.UserId);
+ };
+
+ return {
+ token,
+ groups,
+ isLoggedIn,
+
+ full_name,
+
+ fromUrl,
+ setupDevAdminSession,
+ setupDevUserSession,
+ isAdmin,
+ clearLocalStorage,
+ };
+});
diff --git a/src/utils/marks.ts b/src/utils/marks.ts
index c306823..900b8c4 100644
--- a/src/utils/marks.ts
+++ b/src/utils/marks.ts
@@ -6,4 +6,4 @@ export function formatMark(mark: number | null | undefined): string {
export function getMarkColor(mark: number | null | undefined): string {
if (mark === null || mark === undefined) return 'grey';
return mark > 0 ? 'green' : mark === 0 ? 'grey' : 'red';
-}
\ No newline at end of file
+}