Skip to content

Commit e77a7eb

Browse files
Компактное отображение списка преподов Дубинушки (#87)
* main page ->сокращенный формат * рабочий вариант, но нужно исправлять (есть скролл и звездочки), странная логика с вычислением по одному преподу * Два режима: компактный и полный. При возврате из карточки обратно, режим не сохраняется. * Элемент с разметкой для таблицы компактного режима. * Добавил оглавление таблицы * не отображаю Null-предметы * # -> № в заголовке таблицы * Убрал ошибки по ESLint: использовал слоты без деструктуризации, но с `slotProps` и затем обратился к свойствам через него. * починил отображение плашки "еще n" в графе предметов * обновил название элемента * Добавил стор для преподов и утилс для функции вычесления цвета оценки * store, который хранитинфу о преподах * удалил лишние комменты * убрал elevation, дитруктурировал, вынес отдельные условия + убрал часть функций в utils * поддержка изменений в MainPage * убрал комменты * добавил стор, который хранит состояние списка (таблица или с полный вид) * использование стора, который хранит состояние списка преподов * ref to computed * support handle change * support other handle --------- Co-authored-by: Илья Батуев <batuev.io18@physics.msu.ru>
1 parent 4b63340 commit e77a7eb

6 files changed

Lines changed: 310 additions & 107 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<template>
2+
<v-data-table
3+
:headers="headers"
4+
:items="tableItems"
5+
hide-default-footer
6+
disable-sort
7+
class="lecturer-table"
8+
@click:row="handleRowClick"
9+
>
10+
<template #[`item.rating`]="{ index }">
11+
{{ ratings[index] }}
12+
</template>
13+
14+
<template
15+
#[`item.fullName`]="{
16+
item: {
17+
raw: { last_name, first_name, middle_name },
18+
},
19+
}"
20+
>
21+
<strong>{{ last_name }}</strong> {{ first_name }} {{ middle_name }}
22+
</template>
23+
24+
<template #[`item.subjects`]="{ item }">
25+
<v-chip-group v-if="getFilteredSubjects(item.raw.subjects).length > 0" class="my-1">
26+
<v-chip
27+
v-for="(subject, idx) in getFilteredSubjects(item.raw.subjects).slice(0, 2)"
28+
:key="idx"
29+
size="small"
30+
readonly
31+
:ripple="false"
32+
class="mr-1"
33+
>
34+
{{ subject }}
35+
</v-chip>
36+
<v-chip v-if="isSubjectOverflow(item.raw.subjects)" size="small" readonly :ripple="false">
37+
еще {{ remainingSubjectsCount(item.raw.subjects) }}
38+
</v-chip>
39+
</v-chip-group>
40+
</template>
41+
42+
<template #[`item.comments`]="{ item }">
43+
{{ item.raw.comments?.length || '—' }}
44+
</template>
45+
46+
<template #[`item.mark_general`]="{ item }">
47+
<v-avatar size="30" :color="getMarkColor(item.raw.mark_general)" class="white--text">
48+
{{ formatMark(item.raw.mark_general) }}
49+
</v-avatar>
50+
</template>
51+
</v-data-table>
52+
</template>
53+
54+
<script setup lang="ts">
55+
import { computed } from 'vue';
56+
import { Lecturer } from '@/models';
57+
import { formatMark, getMarkColor } from '@/utils/marks'; // Импорт из нового файла
58+
59+
interface TableItem {
60+
raw: Lecturer;
61+
}
62+
63+
interface CustomDataTableHeader {
64+
title: string;
65+
key: string;
66+
width?: string;
67+
sortable?: boolean;
68+
align?: 'start' | 'center' | 'end';
69+
}
70+
71+
const props = defineProps({
72+
lecturers: { type: Array as () => Lecturer[], required: true },
73+
ratings: { type: Array as () => number[], required: true },
74+
});
75+
76+
const emit = defineEmits(['lecturerClick']);
77+
78+
const headers: CustomDataTableHeader[] = [
79+
{ title: '#', key: 'rating', width: '50px', sortable: false },
80+
{ title: 'ФИО', key: 'fullName', sortable: false },
81+
{ title: 'Предметы', key: 'subjects', sortable: false },
82+
{ title: 'Отзывы', key: 'comments', align: 'center', sortable: false },
83+
{ title: 'Оценка', key: 'mark_general', align: 'center', sortable: false },
84+
];
85+
86+
const tableItems = computed(() => {
87+
if (!props.lecturers) return [];
88+
return props.lecturers.map(lecturer => ({
89+
raw: lecturer,
90+
}));
91+
});
92+
93+
function getFilteredSubjects(subjects: string[] | null | undefined): string[] {
94+
if (!subjects) return [];
95+
return subjects.filter((subject): subject is string => subject !== null);
96+
}
97+
98+
// Условие для отображения плашки "еще N"
99+
function isSubjectOverflow(subjects: string[] | null | undefined): boolean {
100+
return getFilteredSubjects(subjects).length > 2;
101+
}
102+
103+
// Расчет количества оставшихся предметов
104+
function remainingSubjectsCount(subjects: string[] | null | undefined): number {
105+
return getFilteredSubjects(subjects).length - 2;
106+
}
107+
108+
function handleRowClick(event: Event, { item }: { item: TableItem }) {
109+
emit('lecturerClick', item.raw.id);
110+
}
111+
</script>
112+
113+
<style scoped>
114+
.lecturer-table {
115+
cursor: pointer;
116+
}
117+
118+
:deep(.lecturer-table thead th) {
119+
font-weight: bold;
120+
background-color: #f5f5f5;
121+
}
122+
123+
:deep(.lecturer-table tbody tr:hover) {
124+
background-color: rgb(0 0 0 / 4%);
125+
}
126+
</style>

src/pages/LecturerPage.vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import LecturerHeaderCard from '@/components/LecturerHeaderCard.vue';
1010
import { adaptNumeral, getPhoto, copyUrlToClipboard } from '@/utils';
1111
1212
const { mobile } = useDisplay();
13-
1413
const router = useRouter();
1514
1615
const page = ref(1);
@@ -34,7 +33,7 @@ async function loadLecturer() {
3433
id: Number(lecturerId),
3534
},
3635
query: {
37-
info: ['comments', 'mark'],
36+
info: ['comments'],
3837
},
3938
},
4039
});

src/pages/MainPage.vue

Lines changed: 103 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,137 @@
11
<script async setup lang="ts">
2+
import { ref, computed, watch } from 'vue';
3+
import { storeToRefs } from 'pinia';
24
import { router } from '@/router';
3-
import { ref, Ref } from 'vue';
4-
import apiClient from '@/api';
55
import { useProfileStore } from '@/store';
6-
import Placeholder from '@/assets/profile_image_placeholder.webp';
6+
import { useSearchStore } from '@/store/searchStore';
7+
import { useLecturerStore } from '@/store/lecturerStore';
8+
import { useMainPageStateStore } from '@/store/mainPageStateStore'; // Обновленный импорт
9+
import { Order, OrderFromText, Subject } from '@/models';
10+
711
import TheSearchBar from '@/components/TheSearchBar.vue';
812
import TheLecturerSearchCard from '@/components/TheLecturerSearchCard.vue';
9-
import { Lecturer, Order, OrderFromText, Subject } from '@/models';
10-
import { getPhoto } from '@/utils';
11-
import { useSearchStore } from '@/store/searchStore';
13+
import TheLecturerSearchTable from '@/components/TheLecturerSearchTable.vue';
1214
13-
// const
1415
const profileStore = useProfileStore();
1516
const searchStore = useSearchStore();
16-
const userAdmin = ref<boolean>(false);
17-
userAdmin.value = profileStore.isAdmin();
18-
const itemsPerPage = 10;
17+
const lecturerStore = useLecturerStore();
18+
const mainPageStateStore = useMainPageStateStore(); // Обновленная инициализация
19+
20+
const { lecturers, lecturersPhotos, totalPages } = storeToRefs(lecturerStore);
21+
const { isCompactView } = storeToRefs(mainPageStateStore); // Получение состояния из нового стора
1922
20-
// utils
21-
const totalPages: Ref<number> = ref(1);
23+
const userAdmin = computed(() => profileStore.isAdmin());
24+
const itemsPerPage = 10;
2225
23-
// search state
26+
// Параметры поиска
2427
const name = ref(searchStore.name);
25-
const subject: Ref<Subject> = ref(searchStore.subject);
28+
const subject = ref<Subject>(searchStore.subject);
2629
const order = ref(searchStore.order || 'по релевантности');
27-
const orderValues = ref(OrderFromText[order.value as keyof typeof OrderFromText] as Order);
2830
const ascending = ref(searchStore.ascending);
2931
const page = ref(searchStore.page);
3032
31-
const lecturers: Ref<Lecturer[] | undefined> = ref();
32-
const lecturersPhotos = ref<string[]>(Array<string>(itemsPerPage));
33-
34-
await loadLecturers();
35-
36-
function toLecturerPage(id: number) {
37-
searchStore.setParams(name.value, subject.value, order.value, ascending.value, page.value);
38-
router.push({ path: 'lecturer', query: { lecturer_id: id } });
39-
}
40-
41-
async function loadLecturers() {
42-
const offset = (page.value - 1) * itemsPerPage;
43-
const res = await apiClient.GET('/rating/lecturer', {
44-
params: {
45-
query: {
46-
limit: itemsPerPage,
47-
name: name.value,
48-
offset,
49-
info: ['comments', 'mark'],
50-
subject: subject.value,
51-
order_by: orderValues.value,
52-
asc_order: ascending.value,
53-
},
54-
},
33+
// Вычисляем порядковые номера для компактного режима
34+
const lecturerRatings = computed(() => {
35+
if (!lecturers.value) return [];
36+
return lecturers.value.map((_, idx) => (page.value - 1) * itemsPerPage + idx + 1);
37+
});
38+
39+
async function updateLecturersList() {
40+
await lecturerStore.fetchLecturers({
41+
page: page.value,
42+
itemsPerPage,
43+
name: name.value,
44+
subject: subject.value,
45+
orderBy: OrderFromText[order.value as keyof typeof OrderFromText] as Order,
46+
ascending: ascending.value,
5547
});
56-
lecturers.value = res.data?.lecturers;
57-
totalPages.value = res.data?.total ? Math.ceil(res.data?.total / itemsPerPage) : 1;
58-
loadPhotos();
5948
}
6049
61-
function loadPhotos() {
62-
lecturersPhotos.value = lecturers.value?.map(item => getPhoto(item.avatar_link)) ?? [Placeholder];
63-
}
50+
// Загрузка при первой загрузке
51+
await updateLecturersList();
6452
65-
async function findLecturer() {
53+
// Обработчики изменений
54+
async function onSearchParamChange() {
6655
page.value = 1;
67-
await loadLecturers();
56+
await updateLecturersList();
6857
}
6958
70-
async function orderLecturers() {
71-
page.value = 1;
72-
orderValues.value = OrderFromText[order.value as keyof typeof OrderFromText] as Order;
73-
await loadLecturers();
74-
}
59+
watch(page, updateLecturersList);
7560
76-
async function filterLecturers() {
77-
page.value = 1;
78-
await loadLecturers();
61+
function toLecturerPage(id: number) {
62+
searchStore.setParams(name.value, subject.value, order.value, ascending.value, page.value);
63+
router.push({ path: 'lecturer', query: { lecturer_id: id } });
7964
}
8065
81-
async function changeAscOrder() {
82-
page.value = 1;
83-
ascending.value = !ascending.value;
84-
await loadLecturers();
66+
function toggleViewMode() {
67+
mainPageStateStore.toggleCompactView(); // Используем метод из нового стора
8568
}
8669
</script>
8770

8871
<template>
8972
<v-container class="ma-0 py-2">
90-
<v-data-iterator :items="lecturers" :items-per-page="itemsPerPage">
91-
<template #header>
92-
<TheSearchBar
93-
v-model:search-query="name"
94-
v-model:subject="subject"
95-
v-model:order="order"
96-
:is-admin="userAdmin"
97-
:ascending="ascending"
98-
:page="page"
99-
@update:subject="filterLecturers"
100-
@update:order="orderLecturers"
101-
@update:search-query="findLecturer"
102-
@changed-asc-desc="changeAscOrder"
103-
/>
104-
</template>
105-
106-
<template #default="{ items }">
107-
<TheLecturerSearchCard
108-
v-for="(item, idx) in items"
109-
:key="idx"
110-
:lecturer="item.raw"
111-
:photo="lecturersPhotos[idx]"
112-
:rating="(page - 1) * itemsPerPage + idx + 1"
113-
class="py-0"
114-
variant="elevated"
115-
@click="toLecturerPage(item.raw.id)"
116-
/>
117-
</template>
118-
119-
<template #no-data>
120-
<div class="ma-2">Ничего не нашли :(</div>
121-
</template>
122-
123-
<template #footer>
124-
<div v-if="lecturers && totalPages > 1">
125-
<v-pagination
126-
v-model="page"
127-
active-color="primary"
73+
<TheSearchBar
74+
v-model:search-query="name"
75+
v-model:subject="subject"
76+
v-model:order="order"
77+
:is-admin="userAdmin"
78+
:ascending="ascending"
79+
:page="page"
80+
@update:subject="onSearchParamChange"
81+
@update:order="onSearchParamChange"
82+
@update:search-query="onSearchParamChange"
83+
@changed-asc-desc="
84+
() => {
85+
ascending = !ascending;
86+
onSearchParamChange();
87+
}
88+
"
89+
/>
90+
91+
<div v-if="!isCompactView">
92+
<v-data-iterator :items="lecturers" :items-per-page="itemsPerPage">
93+
<template #default="{ items }">
94+
<TheLecturerSearchCard
95+
v-for="(item, idx) in items"
96+
:key="idx"
97+
:lecturer="item.raw"
98+
:photo="lecturersPhotos[idx]"
99+
:rating="(page - 1) * itemsPerPage + idx + 1"
100+
class="py-0"
128101
variant="elevated"
129-
:length="totalPages"
130-
:total-visible="1"
131-
:show-first-last-page="true"
132-
ellipsis=""
133-
@update:model-value="loadLecturers"
102+
@click="toLecturerPage(item.raw.id)"
134103
/>
135-
</div>
136-
</template>
137-
</v-data-iterator>
104+
</template>
105+
<template #no-data>
106+
<div class="ma-2">Ничего не нашли :(</div>
107+
</template>
108+
</v-data-iterator>
109+
</div>
110+
111+
<div v-else>
112+
<TheLecturerSearchTable
113+
v-if="lecturers && lecturers.length > 0"
114+
:lecturers="lecturers"
115+
:ratings="lecturerRatings"
116+
@lecturer-click="toLecturerPage"
117+
/>
118+
<div v-else class="ma-2">Ничего не нашли :(</div>
119+
</div>
120+
121+
<div v-if="lecturers && totalPages > 1" class="d-flex align-center justify-center mt-4">
122+
<v-btn icon class="mr-2" :title="isCompactView ? 'Обычный вид' : 'Компактный вид'" @click="toggleViewMode">
123+
<v-icon>{{ isCompactView ? 'mdi-view-agenda' : 'mdi-table' }}</v-icon>
124+
</v-btn>
125+
126+
<v-pagination
127+
v-model="page"
128+
active-color="primary"
129+
variant="elevated"
130+
:length="totalPages"
131+
:total-visible="1"
132+
:show-first-last-page="true"
133+
ellipsis=""
134+
/>
135+
</div>
138136
</v-container>
139137
</template>

0 commit comments

Comments
 (0)