fix: resolve education dashboard interface issues

This commit is contained in:
Server Migration
2026-08-15 13:06:49 +03:30
parent af0149d722
commit d94a96b25e
3 changed files with 98 additions and 23 deletions
+1 -4
View File
@@ -9,10 +9,7 @@ export const courseSchema = object().shape({
.integer('ظرفیت باید عدد صحیح باشد') .integer('ظرفیت باید عدد صحیح باشد')
.min(1, 'ظرفیت باید حداقل ۱ نفر باشد'), .min(1, 'ظرفیت باید حداقل ۱ نفر باشد'),
prerequisiteCourseIds: array().nullable().default([]), prerequisiteCourseIds: array().nullable().default([]),
contentMediaId: number() coverMediaId: number().typeError('شناسه تصویر دوره باید عدد باشد').nullable().notRequired(),
.typeError('شناسه فایل محتوای دوره باید عدد باشد')
.nullable()
.notRequired(),
description: string().nullable().notRequired(), description: string().nullable().notRequired(),
termId: mixed().required(), termId: mixed().required(),
isActive: boolean().nullable().notRequired(), isActive: boolean().nullable().notRequired(),
@@ -91,11 +91,11 @@ const performanceStats = computed(() => {
const stats = dashboard.value.stats ?? {} const stats = dashboard.value.stats ?? {}
return [ return [
{ {
icon: 'user-sound', icon: 'video',
label: 'تعداد جلسات اعزام شده', label: 'تعداد جلسات آموزشی گذرانده‌شده',
value: toFaDigits(stats.attendedSessions ?? 0), value: toFaDigits(stats.attendedSessions ?? 0),
unit: 'جلسه', unit: 'جلسه',
subtitle: 'اعزام شده است', subtitle: 'با موفقیت گذرانده شده',
to: { name: 'student-terms' }, to: { name: 'student-terms' },
}, },
{ {
+84 -6
View File
@@ -5,17 +5,36 @@
</div> </div>
<nav class="admin-sidebar__nav"> <nav class="admin-sidebar__nav">
<ul> <ul>
<li v-for="item in menuItems" :key="item.title" class="admin-sidebar__item"> <li v-for="(item, index) in menuItems" :key="item.title" class="admin-sidebar__item">
<component <button
:is="item.to ? 'RouterLink' : 'div'" v-if="item.children?.length"
type="button"
class="admin-sidebar__link"
:class="{ 'admin-sidebar__link--active': item.active }"
:aria-expanded="isExpanded(item)"
:aria-controls="submenuId(index)"
@click="toggle(item)"
>
<SvgIcon v-if="item.icon" :name="item.icon" :size="22" />
<span>{{ item.title }}</span>
<SvgIcon
name="caret-down"
:size="16"
class="admin-sidebar__chevron"
:class="{ 'admin-sidebar__chevron--expanded': isExpanded(item) }"
/>
</button>
<RouterLink
v-else
:to="item.to" :to="item.to"
class="admin-sidebar__link" class="admin-sidebar__link"
:class="{ 'admin-sidebar__link--active': item.active }" :class="{ 'admin-sidebar__link--active': item.active }"
> >
<SvgIcon v-if="item.icon" :name="item.icon" :size="22" /> <SvgIcon v-if="item.icon" :name="item.icon" :size="22" />
<span>{{ item.title }}</span> <span>{{ item.title }}</span>
</component> </RouterLink>
<ul v-if="item.children?.length" class="admin-sidebar__submenu"> <Transition v-if="item.children?.length" name="admin-sidebar__accordion">
<ul v-show="isExpanded(item)" :id="submenuId(index)" class="admin-sidebar__submenu">
<li v-for="child in item.children" :key="child.title"> <li v-for="child in item.children" :key="child.title">
<RouterLink <RouterLink
:to="child.to" :to="child.to"
@@ -26,6 +45,7 @@
</RouterLink> </RouterLink>
</li> </li>
</ul> </ul>
</Transition>
</li> </li>
</ul> </ul>
</nav> </nav>
@@ -34,16 +54,43 @@
</template> </template>
<script setup> <script setup>
import { ref, watch } from 'vue'
import { RouterLink } from 'vue-router' import { RouterLink } from 'vue-router'
import { gallery } from '@/utils/gallery' import { gallery } from '@/utils/gallery'
import SvgIcon from '@/components/icons/SvgIcon.vue' import SvgIcon from '@/components/icons/SvgIcon.vue'
import SidebarLogout from '@/layouts/sidebars/SidebarLogout.vue' import SidebarLogout from '@/layouts/sidebars/SidebarLogout.vue'
defineProps({ const props = defineProps({
menuItems: { type: Array, default: () => [] }, menuItems: { type: Array, default: () => [] },
}) })
const logo = gallery.logoPinkishRed const logo = gallery.logoPinkishRed
const expandedItems = ref({})
watch(
() => props.menuItems,
(items) => {
const next = { ...expandedItems.value }
items.forEach((item) => {
if (!item.children?.length) return
if (!(item.title in next)) next[item.title] = false
if (item.active) next[item.title] = true
})
expandedItems.value = next
},
{ immediate: true }
)
const isExpanded = (item) => Boolean(expandedItems.value[item.title])
const toggle = (item) => {
expandedItems.value = {
...expandedItems.value,
[item.title]: !isExpanded(item),
}
}
const submenuId = (index) => `admin-sidebar-submenu-${index}`
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -82,6 +129,7 @@ const logo = gallery.logoPinkishRed
} }
&__link { &__link {
width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.625rem; gap: 0.625rem;
@@ -91,6 +139,9 @@ const logo = gallery.logoPinkishRed
font-size: 0.875rem; font-size: 0.875rem;
color: #4b5563; color: #4b5563;
text-decoration: none; text-decoration: none;
text-align: start;
border: 0;
background: transparent;
transition: background-color 0.15s, color 0.15s; transition: background-color 0.15s, color 0.15s;
cursor: pointer; cursor: pointer;
@@ -105,6 +156,15 @@ const logo = gallery.logoPinkishRed
} }
} }
&__chevron {
margin-inline-start: auto;
transition: transform 0.2s ease;
&--expanded {
transform: rotate(180deg);
}
}
&__submenu { &__submenu {
margin-top: 0.25rem !important; margin-top: 0.25rem !important;
padding-inline-start: 1.5rem !important; padding-inline-start: 1.5rem !important;
@@ -130,5 +190,23 @@ const logo = gallery.logoPinkishRed
font-weight: 700; font-weight: 700;
} }
} }
&__accordion-enter-active,
&__accordion-leave-active {
overflow: hidden;
transition: max-height 0.2s ease, opacity 0.2s ease;
}
&__accordion-enter-from,
&__accordion-leave-to {
max-height: 0;
opacity: 0;
}
&__accordion-enter-to,
&__accordion-leave-from {
max-height: 16rem;
opacity: 1;
}
} }
</style> </style>