Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9410881
main page ->сокращенный формат
Dmatrushka19 Aug 21, 2025
71c0799
рабочий вариант, но нужно исправлять (есть скролл и звездочки), стран…
Dmatrushka19 Aug 21, 2025
866b8e1
Два режима: компактный и полный. При возврате из карточки обратно, ре…
Dmatrushka19 Aug 22, 2025
ffcc9c5
Элемент с разметкой для таблицы компактного режима.
Dmatrushka19 Aug 22, 2025
02b0b4c
Добавил оглавление таблицы
Dmatrushka19 Aug 22, 2025
b052004
не отображаю Null-предметы
Dmatrushka19 Aug 22, 2025
fdecca8
# -> № в заголовке таблицы
Dmatrushka19 Aug 22, 2025
ea80a36
Убрал ошибки по ESLint: использовал слоты без деструктуризации, но с …
Dmatrushka19 Aug 22, 2025
073839f
починил отображение плашки "еще n" в графе предметов
Dmatrushka19 Aug 22, 2025
442a538
обновил название элемента
Dmatrushka19 Sep 2, 2025
6c0db48
Добавил стор для преподов и утилс для функции вычесления цвета оценки
Dmatrushka19 Sep 2, 2025
0945746
store, который хранитинфу о преподах
Dmatrushka19 Sep 2, 2025
8d60319
удалил лишние комменты
Dmatrushka19 Sep 2, 2025
a807fc3
убрал elevation, дитруктурировал, вынес отдельные условия + убрал час…
Dmatrushka19 Sep 2, 2025
1c2e050
поддержка изменений в MainPage
Dmatrushka19 Sep 2, 2025
836c05a
убрал комменты
Dmatrushka19 Sep 2, 2025
1670d9f
добавил стор, который хранит состояние списка (таблица или с полный вид)
Dmatrushka19 Sep 2, 2025
f45d9d4
использование стора, который хранит состояние списка преподов
Dmatrushka19 Sep 2, 2025
1fcf818
ref to computed
BatuevIO Sep 16, 2025
141bdc9
support handle change
BatuevIO Sep 16, 2025
f5146bd
support other handle
BatuevIO Sep 16, 2025
78bf718
Merge branch 'main' into Compact-view-of-lecturer-search-screen
BatuevIO Sep 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions src/components/TheLecturerSearchTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<template>
<v-data-table
:headers="headers"
:items="tableItems"
hide-default-footer
disable-sort
class="lecturer-table"
@click:row="handleRowClick"
>
<template #[`item.rating`]="{ index }">
{{ ratings[index] }}
</template>

<template
#[`item.fullName`]="{
item: {
raw: { last_name, first_name, middle_name },
},
}"
>
<strong>{{ last_name }}</strong> {{ first_name }} {{ middle_name }}
</template>

<template #[`item.subjects`]="{ item }">
<v-chip-group v-if="getFilteredSubjects(item.raw.subjects).length > 0" class="my-1">
<v-chip
v-for="(subject, idx) in getFilteredSubjects(item.raw.subjects).slice(0, 2)"
:key="idx"
size="small"
readonly
:ripple="false"
class="mr-1"
>
{{ subject }}
</v-chip>
<v-chip v-if="isSubjectOverflow(item.raw.subjects)" size="small" readonly :ripple="false">
еще {{ remainingSubjectsCount(item.raw.subjects) }}
</v-chip>
</v-chip-group>
</template>

<template #[`item.comments`]="{ item }">
{{ item.raw.comments?.length || '—' }}
</template>

<template #[`item.mark_general`]="{ item }">
<v-avatar size="30" :color="getMarkColor(item.raw.mark_general)" class="white--text">
{{ formatMark(item.raw.mark_general) }}
</v-avatar>
</template>
</v-data-table>
</template>

<script setup lang="ts">
import { computed } from 'vue';
import { Lecturer } from '@/models';
import { formatMark, getMarkColor } from '@/utils/marks'; // Импорт из нового файла

interface TableItem {
raw: Lecturer;
}

interface CustomDataTableHeader {
title: string;
key: string;
width?: string;
sortable?: boolean;
align?: 'start' | 'center' | 'end';
}

const props = defineProps({
lecturers: { type: Array as () => Lecturer[], required: true },
ratings: { type: Array as () => number[], required: true },
});

const emit = defineEmits(['lecturerClick']);

const headers: CustomDataTableHeader[] = [
{ title: '#', key: 'rating', width: '50px', sortable: false },
{ 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 },
];

const tableItems = computed(() => {
if (!props.lecturers) return [];
return props.lecturers.map(lecturer => ({
raw: lecturer,
}));
});

function getFilteredSubjects(subjects: string[] | null | undefined): string[] {
if (!subjects) return [];
return subjects.filter((subject): subject is string => subject !== null);
}

// Условие для отображения плашки "еще N"
function isSubjectOverflow(subjects: string[] | null | undefined): boolean {
return getFilteredSubjects(subjects).length > 2;
}

// Расчет количества оставшихся предметов
function remainingSubjectsCount(subjects: string[] | null | undefined): number {
return getFilteredSubjects(subjects).length - 2;
}

function handleRowClick(event: Event, { item }: { item: TableItem }) {
emit('lecturerClick', item.raw.id);
}
</script>

<style scoped>
.lecturer-table {
cursor: pointer;
}

:deep(.lecturer-table thead th) {
font-weight: bold;
background-color: #f5f5f5;
}

:deep(.lecturer-table tbody tr:hover) {
background-color: rgb(0 0 0 / 4%);
}
</style>
3 changes: 1 addition & 2 deletions src/pages/LecturerPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import LecturerHeaderCard from '@/components/LecturerHeaderCard.vue';
import { adaptNumeral, getPhoto, copyUrlToClipboard } from '@/utils';

const { mobile } = useDisplay();

const router = useRouter();

const page = ref(1);
Expand All @@ -34,7 +33,7 @@ async function loadLecturer() {
id: Number(lecturerId),
},
query: {
info: ['comments', 'mark'],
info: ['comments'],
},
},
});
Expand Down
208 changes: 103 additions & 105 deletions src/pages/MainPage.vue
Original file line number Diff line number Diff line change
@@ -1,139 +1,137 @@
<script async setup lang="ts">
import { ref, computed, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { router } from '@/router';
import { ref, Ref } from 'vue';
import apiClient from '@/api';
import { useProfileStore } from '@/store';
import Placeholder from '@/assets/profile_image_placeholder.webp';
import { useSearchStore } from '@/store/searchStore';
import { useLecturerStore } from '@/store/lecturerStore';
import { useMainPageStateStore } from '@/store/mainPageStateStore'; // Обновленный импорт
import { Order, OrderFromText, Subject } from '@/models';

import TheSearchBar from '@/components/TheSearchBar.vue';
import TheLecturerSearchCard from '@/components/TheLecturerSearchCard.vue';
import { Lecturer, Order, OrderFromText, Subject } from '@/models';
import { getPhoto } from '@/utils';
import { useSearchStore } from '@/store/searchStore';
import TheLecturerSearchTable from '@/components/TheLecturerSearchTable.vue';

// const
const profileStore = useProfileStore();
const searchStore = useSearchStore();
const userAdmin = ref<boolean>(false);
userAdmin.value = profileStore.isAdmin();
const itemsPerPage = 10;
const lecturerStore = useLecturerStore();
const mainPageStateStore = useMainPageStateStore(); // Обновленная инициализация

const { lecturers, lecturersPhotos, totalPages } = storeToRefs(lecturerStore);
const { isCompactView } = storeToRefs(mainPageStateStore); // Получение состояния из нового стора

// utils
const totalPages: Ref<number> = ref(1);
const userAdmin = computed(() => profileStore.isAdmin());
const itemsPerPage = 10;

// search state
// Параметры поиска
const name = ref(searchStore.name);
const subject: Ref<Subject> = ref(searchStore.subject);
const subject = ref<Subject>(searchStore.subject);
const order = ref(searchStore.order || 'по релевантности');
const orderValues = ref(OrderFromText[order.value as keyof typeof OrderFromText] as Order);
const ascending = ref(searchStore.ascending);
const page = ref(searchStore.page);

const lecturers: Ref<Lecturer[] | undefined> = ref();
const lecturersPhotos = ref<string[]>(Array<string>(itemsPerPage));

await loadLecturers();

function toLecturerPage(id: number) {
searchStore.setParams(name.value, subject.value, order.value, ascending.value, page.value);
router.push({ path: 'lecturer', query: { lecturer_id: id } });
}

async function loadLecturers() {
const offset = (page.value - 1) * itemsPerPage;
const res = await apiClient.GET('/rating/lecturer', {
params: {
query: {
limit: itemsPerPage,
name: name.value,
offset,
info: ['comments', 'mark'],
subject: subject.value,
order_by: orderValues.value,
asc_order: ascending.value,
},
},
// Вычисляем порядковые номера для компактного режима
const lecturerRatings = computed(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тоже хорошо будет убрать в стор

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Это можно сделать не сейчас

if (!lecturers.value) return [];
return lecturers.value.map((_, idx) => (page.value - 1) * itemsPerPage + idx + 1);
});

async function updateLecturersList() {
await lecturerStore.fetchLecturers({
page: page.value,
itemsPerPage,
name: name.value,
subject: subject.value,
orderBy: OrderFromText[order.value as keyof typeof OrderFromText] as Order,
ascending: ascending.value,
});
lecturers.value = res.data?.lecturers;
totalPages.value = res.data?.total ? Math.ceil(res.data?.total / itemsPerPage) : 1;
loadPhotos();
}

function loadPhotos() {
lecturersPhotos.value = lecturers.value?.map(item => getPhoto(item.avatar_link)) ?? [Placeholder];
}
// Загрузка при первой загрузке
await updateLecturersList();

async function findLecturer() {
// Обработчики изменений
async function onSearchParamChange() {
page.value = 1;
await loadLecturers();
await updateLecturersList();
}

async function orderLecturers() {
page.value = 1;
orderValues.value = OrderFromText[order.value as keyof typeof OrderFromText] as Order;
await loadLecturers();
}
watch(page, updateLecturersList);

async function filterLecturers() {
page.value = 1;
await loadLecturers();
function toLecturerPage(id: number) {
searchStore.setParams(name.value, subject.value, order.value, ascending.value, page.value);
router.push({ path: 'lecturer', query: { lecturer_id: id } });
}

async function changeAscOrder() {
page.value = 1;
ascending.value = !ascending.value;
await loadLecturers();
function toggleViewMode() {
mainPageStateStore.toggleCompactView(); // Используем метод из нового стора
}
</script>

<template>
<v-container class="ma-0 py-2">
<v-data-iterator :items="lecturers" :items-per-page="itemsPerPage">
<template #header>
<TheSearchBar
v-model:search-query="name"
v-model:subject="subject"
v-model:order="order"
:is-admin="userAdmin"
:ascending="ascending"
:page="page"
@update:subject="filterLecturers"
@update:order="orderLecturers"
@update:search-query="findLecturer"
@changed-asc-desc="changeAscOrder"
/>
</template>

<template #default="{ items }">
<TheLecturerSearchCard
v-for="(item, idx) in items"
:key="idx"
:lecturer="item.raw"
:photo="lecturersPhotos[idx]"
:rating="(page - 1) * itemsPerPage + idx + 1"
class="py-0"
variant="elevated"
@click="toLecturerPage(item.raw.id)"
/>
</template>

<template #no-data>
<div class="ma-2">Ничего не нашли :(</div>
</template>

<template #footer>
<div v-if="lecturers && totalPages > 1">
<v-pagination
v-model="page"
active-color="primary"
<TheSearchBar
v-model:search-query="name"
v-model:subject="subject"
v-model:order="order"
:is-admin="userAdmin"
:ascending="ascending"
:page="page"
@update:subject="onSearchParamChange"
@update:order="onSearchParamChange"
@update:search-query="onSearchParamChange"
@changed-asc-desc="
() => {
ascending = !ascending;
onSearchParamChange();
}
"
/>

<div v-if="!isCompactView">
<v-data-iterator :items="lecturers" :items-per-page="itemsPerPage">
<template #default="{ items }">
<TheLecturerSearchCard
v-for="(item, idx) in items"
:key="idx"
:lecturer="item.raw"
:photo="lecturersPhotos[idx]"
:rating="(page - 1) * itemsPerPage + idx + 1"
class="py-0"
variant="elevated"
:length="totalPages"
:total-visible="1"
:show-first-last-page="true"
ellipsis=""
@update:model-value="loadLecturers"
@click="toLecturerPage(item.raw.id)"
/>
</div>
</template>
</v-data-iterator>
</template>
<template #no-data>
<div class="ma-2">Ничего не нашли :(</div>
</template>
</v-data-iterator>
</div>

<div v-else>
<TheLecturerSearchTable
v-if="lecturers && lecturers.length > 0"
:lecturers="lecturers"
:ratings="lecturerRatings"
@lecturer-click="toLecturerPage"
/>
<div v-else class="ma-2">Ничего не нашли :(</div>
</div>

<div v-if="lecturers && totalPages > 1" class="d-flex align-center justify-center mt-4">
<v-btn icon class="mr-2" :title="isCompactView ? 'Обычный вид' : 'Компактный вид'" @click="toggleViewMode">
<v-icon>{{ isCompactView ? 'mdi-view-agenda' : 'mdi-table' }}</v-icon>
</v-btn>

<v-pagination
v-model="page"
active-color="primary"
variant="elevated"
:length="totalPages"
:total-visible="1"
:show-first-last-page="true"
ellipsis=""
/>
</div>
</v-container>
</template>
Loading