Initialize the admin app with banner management for app sections.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,729 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { onClickOutside, useEventListener } from '@vueuse/core'
|
||||
import AdminIcon from '@/components/icons/AdminIcon.vue'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { dashboardApi, notificationsApi } from '@/services/api'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import { resolveAdminInboxLink } from '@/utils/inboxLinks'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useThemeStore, type ThemePreference } from '@/stores/theme'
|
||||
import type { AdminNotification, InternalMessage } from '@/types'
|
||||
|
||||
defineEmits<{
|
||||
menuClick: []
|
||||
}>()
|
||||
|
||||
type OpenPanel = 'messages' | 'notifications' | 'theme' | null
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const theme = useThemeStore()
|
||||
const openPanel = ref<OpenPanel>(null)
|
||||
const actionsRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const messageList = ref<InternalMessage[]>([])
|
||||
const notificationList = ref<AdminNotification[]>([])
|
||||
|
||||
const unreadMessages = computed(() => messageList.value.filter((m) => !m.isRead).length)
|
||||
const unreadNotifications = computed(() => notificationList.value.filter((n) => !n.isRead).length)
|
||||
|
||||
const themeOptions: { value: ThemePreference; label: string; icon: string }[] = [
|
||||
{ value: 'light', label: 'روشن', icon: 'vuesax-linear:sun-1' },
|
||||
{ value: 'dark', label: 'تاریک', icon: 'vuesax-linear:moon' },
|
||||
{ value: 'system', label: 'سیستم', icon: 'vuesax-linear:mobile' },
|
||||
]
|
||||
|
||||
const themeLabel = computed(
|
||||
() => themeOptions.find((o) => o.value === theme.preference)?.label ?? 'سیستم',
|
||||
)
|
||||
|
||||
const themeTriggerIcon = computed(
|
||||
() => themeOptions.find((o) => o.value === theme.preference)?.icon ?? 'vuesax-linear:sun-1',
|
||||
)
|
||||
|
||||
void dashboardApi
|
||||
.get()
|
||||
.then((data) => {
|
||||
messageList.value = data.messages ?? []
|
||||
notificationList.value = data.notifications ?? []
|
||||
})
|
||||
.catch(() => {
|
||||
messageList.value = []
|
||||
notificationList.value = []
|
||||
})
|
||||
|
||||
onClickOutside(actionsRef, () => {
|
||||
openPanel.value = null
|
||||
})
|
||||
|
||||
useEventListener(document, 'keydown', (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') openPanel.value = null
|
||||
})
|
||||
|
||||
function togglePanel(panel: Exclude<OpenPanel, null>) {
|
||||
openPanel.value = openPanel.value === panel ? null : panel
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
openPanel.value = null
|
||||
}
|
||||
|
||||
function markMessageRead(message: InternalMessage) {
|
||||
message.isRead = true
|
||||
}
|
||||
|
||||
function markNotificationRead(notification: AdminNotification) {
|
||||
if (notification.isRead) return
|
||||
notification.isRead = true
|
||||
void notificationsApi.markRead(notification.id).catch(() => {
|
||||
notification.isRead = false
|
||||
})
|
||||
}
|
||||
|
||||
function markAllMessagesRead() {
|
||||
messageList.value.forEach((m) => {
|
||||
m.isRead = true
|
||||
})
|
||||
}
|
||||
|
||||
function markAllNotificationsRead() {
|
||||
const previouslyUnread = notificationList.value.filter((n) => !n.isRead)
|
||||
if (!previouslyUnread.length) return
|
||||
previouslyUnread.forEach((n) => {
|
||||
n.isRead = true
|
||||
})
|
||||
void notificationsApi.markAllRead().catch(() => {
|
||||
previouslyUnread.forEach((n) => {
|
||||
n.isRead = false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function navigateInboxTarget(target: string | null) {
|
||||
closePanel()
|
||||
if (!target) return
|
||||
void router.push(target)
|
||||
}
|
||||
|
||||
function openMessage(message: InternalMessage) {
|
||||
markMessageRead(message)
|
||||
navigateInboxTarget(
|
||||
resolveAdminInboxLink({
|
||||
link: message.link,
|
||||
orderId: message.orderId,
|
||||
title: message.subject,
|
||||
body: message.preview,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function openNotification(notification: AdminNotification) {
|
||||
markNotificationRead(notification)
|
||||
navigateInboxTarget(
|
||||
resolveAdminInboxLink({
|
||||
link: notification.link,
|
||||
orderId: notification.orderId,
|
||||
title: notification.title,
|
||||
body: notification.body,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function selectTheme(value: ThemePreference) {
|
||||
theme.setPreference(value)
|
||||
closePanel()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="admin-header">
|
||||
<div class="admin-header__right">
|
||||
<button
|
||||
type="button"
|
||||
class="admin-header__menu-btn compact-only"
|
||||
aria-label="باز کردن منو"
|
||||
@click="$emit('menuClick')"
|
||||
>
|
||||
<AdminIcon name="menu" :size="22" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div ref="actionsRef" class="admin-header__left">
|
||||
<div class="admin-header__dropdown">
|
||||
<button
|
||||
type="button"
|
||||
class="admin-header__icon-btn"
|
||||
:class="{ 'admin-header__icon-btn--active': openPanel === 'messages' }"
|
||||
title="پیامهای داخلی"
|
||||
:aria-label="`پیامهای داخلی — ${unreadMessages} پیام خواندهنشده`"
|
||||
:aria-expanded="openPanel === 'messages'"
|
||||
aria-haspopup="true"
|
||||
@click.stop="togglePanel('messages')"
|
||||
>
|
||||
<AdminIcon name="message" :size="22" />
|
||||
<span v-if="unreadMessages" class="admin-header__badge" aria-hidden="true">
|
||||
{{ unreadMessages }}
|
||||
</span>
|
||||
<span class="admin-header__icon-label">پیام</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="openPanel === 'messages'"
|
||||
class="admin-header__panel"
|
||||
role="dialog"
|
||||
aria-label="پیامهای داخلی"
|
||||
@click.stop
|
||||
>
|
||||
<div class="admin-header__panel-header">
|
||||
<h2 class="admin-header__panel-title">پیامهای داخلی</h2>
|
||||
<button
|
||||
v-if="unreadMessages"
|
||||
type="button"
|
||||
class="admin-header__panel-action"
|
||||
@click="markAllMessagesRead"
|
||||
>
|
||||
علامتگذاری همه بهعنوان خواندهشده
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul v-if="messageList.length" class="admin-header__panel-list">
|
||||
<li
|
||||
v-for="message in messageList"
|
||||
:key="message.id"
|
||||
class="admin-header__panel-item"
|
||||
:class="{ unread: !message.isRead }"
|
||||
>
|
||||
<button type="button" class="admin-header__panel-item-btn" @click="openMessage(message)">
|
||||
<span class="admin-header__panel-item-top">
|
||||
<strong>{{ message.subject }}</strong>
|
||||
<time :datetime="message.sentAt">{{ formatDateTime(message.sentAt) }}</time>
|
||||
</span>
|
||||
<span class="admin-header__panel-item-sender">از {{ message.senderName }}</span>
|
||||
<span class="admin-header__panel-item-body">{{ message.preview }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p v-else class="admin-header__panel-empty">پیامی وجود ندارد.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-header__dropdown">
|
||||
<button
|
||||
type="button"
|
||||
class="admin-header__icon-btn"
|
||||
:class="{ 'admin-header__icon-btn--active': openPanel === 'notifications' }"
|
||||
title="اعلانها"
|
||||
:aria-label="`اعلانها — ${unreadNotifications} اعلان جدید`"
|
||||
:aria-expanded="openPanel === 'notifications'"
|
||||
aria-haspopup="true"
|
||||
@click.stop="togglePanel('notifications')"
|
||||
>
|
||||
<AdminIcon name="bell" :size="22" />
|
||||
<span v-if="unreadNotifications" class="admin-header__badge" aria-hidden="true">
|
||||
{{ unreadNotifications }}
|
||||
</span>
|
||||
<span class="admin-header__icon-label">اعلان</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="openPanel === 'notifications'"
|
||||
class="admin-header__panel"
|
||||
role="dialog"
|
||||
aria-label="اعلانها"
|
||||
@click.stop
|
||||
>
|
||||
<div class="admin-header__panel-header">
|
||||
<h2 class="admin-header__panel-title">اعلانها</h2>
|
||||
<button
|
||||
v-if="unreadNotifications"
|
||||
type="button"
|
||||
class="admin-header__panel-action"
|
||||
@click="markAllNotificationsRead"
|
||||
>
|
||||
علامتگذاری همه بهعنوان خواندهشده
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul v-if="notificationList.length" class="admin-header__panel-list">
|
||||
<li
|
||||
v-for="notification in notificationList"
|
||||
:key="notification.id"
|
||||
class="admin-header__panel-item"
|
||||
:class="[
|
||||
{ unread: !notification.isRead },
|
||||
notification.tone ? `admin-header__panel-item--${notification.tone}` : '',
|
||||
]"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="admin-header__panel-item-btn"
|
||||
@click="openNotification(notification)"
|
||||
>
|
||||
<span class="admin-header__panel-item-top">
|
||||
<strong>{{ notification.title }}</strong>
|
||||
<time :datetime="notification.sentAt">{{ formatDateTime(notification.sentAt) }}</time>
|
||||
</span>
|
||||
<span class="admin-header__panel-item-body">{{ notification.body }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p v-else class="admin-header__panel-empty">اعلانی وجود ندارد.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-header__dropdown">
|
||||
<button
|
||||
type="button"
|
||||
class="admin-header__icon-btn"
|
||||
:class="{ 'admin-header__icon-btn--active': openPanel === 'theme' }"
|
||||
title="حالت نمایش"
|
||||
:aria-label="`حالت نمایش — ${themeLabel}`"
|
||||
:aria-expanded="openPanel === 'theme'"
|
||||
aria-haspopup="true"
|
||||
@click.stop="togglePanel('theme')"
|
||||
>
|
||||
<span class="admin-header__theme-glyph" aria-hidden="true">
|
||||
<BasicIcon :icon="themeTriggerIcon" :size="20" :stroke-width="1.5" />
|
||||
</span>
|
||||
<span class="admin-header__icon-label">تم</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="openPanel === 'theme'"
|
||||
class="admin-header__panel admin-header__panel--theme"
|
||||
role="dialog"
|
||||
aria-label="انتخاب حالت نمایش"
|
||||
@click.stop
|
||||
>
|
||||
<div class="admin-header__panel-header">
|
||||
<h2 class="admin-header__panel-title">حالت نمایش</h2>
|
||||
</div>
|
||||
<ul class="admin-header__theme-list">
|
||||
<li v-for="option in themeOptions" :key="option.value">
|
||||
<button
|
||||
type="button"
|
||||
class="admin-header__theme-option"
|
||||
:class="{ 'admin-header__theme-option--active': theme.preference === option.value }"
|
||||
@click="selectTheme(option.value)"
|
||||
>
|
||||
<span class="admin-header__theme-option-icon" aria-hidden="true">
|
||||
<BasicIcon :icon="option.icon" :size="18" :stroke-width="1.5" />
|
||||
</span>
|
||||
<span class="admin-header__theme-option-label">{{ option.label }}</span>
|
||||
<BasicIcon
|
||||
v-if="theme.preference === option.value"
|
||||
class="admin-header__theme-option-check"
|
||||
icon="vuesax-linear:tick-circle"
|
||||
:size="16"
|
||||
:stroke-width="1.75"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-header__user">
|
||||
<div class="admin-header__avatar">{{ auth.fullName.charAt(0) || '؟' }}</div>
|
||||
<div class="admin-header__user-info">
|
||||
<strong>{{ auth.fullName || 'مدیر' }}</strong>
|
||||
<span>{{ auth.roleLabel || 'پنل مدیریت' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-header {
|
||||
height: var(--header-height);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 0 28px;
|
||||
background: var(--color-surface);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.admin-header__right {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-header__menu-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.admin-header__menu-btn:hover {
|
||||
background: var(--color-border-light);
|
||||
}
|
||||
|
||||
.admin-header__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-header__dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.admin-header__icon-btn {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
min-width: 52px;
|
||||
height: 52px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.admin-header__icon-btn:hover,
|
||||
.admin-header__icon-btn--active {
|
||||
background: var(--color-border-light);
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.admin-header__icon-label {
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.admin-header__badge {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-danger);
|
||||
color: #fff;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid var(--color-surface);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.admin-header__panel {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
left: 0;
|
||||
z-index: 950;
|
||||
width: min(360px, calc(100vw - 32px));
|
||||
max-height: min(420px, calc(100dvh - var(--header-height) - 24px));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-card);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-header__panel-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.admin-header__panel-title {
|
||||
margin: 0;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.admin-header__panel-action {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-primary);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-header__panel-action:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.admin-header__panel-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.admin-header__panel-item {
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.admin-header__panel-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.admin-header__panel-item.unread {
|
||||
background: var(--color-info-light);
|
||||
}
|
||||
|
||||
.admin-header__panel-item-btn {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-align: right;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.admin-header__panel-item-btn:hover {
|
||||
background: var(--color-border-light);
|
||||
}
|
||||
|
||||
.admin-header__panel-item-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-header__panel-item-top strong {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.admin-header__panel-item-top time {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.admin-header__panel-item-sender {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.admin-header__panel-item-body {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.admin-header__panel-item--warning.unread {
|
||||
background: var(--color-warning-light);
|
||||
}
|
||||
|
||||
.admin-header__panel-item--danger.unread {
|
||||
background: var(--color-danger-light);
|
||||
}
|
||||
|
||||
.admin-header__panel-item--success.unread {
|
||||
background: var(--color-success-light);
|
||||
}
|
||||
|
||||
.admin-header__panel-empty {
|
||||
margin: 0;
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.admin-header__user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding-right: 12px;
|
||||
margin-right: 4px;
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.admin-header__avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.admin-header__user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.admin-header__user-info strong {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-header__user-info span {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.admin-header__theme-glyph {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.admin-header__panel--theme {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.admin-header__theme-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.admin-header__theme-option {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-align: right;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 12px;
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
padding: 10px 12px;
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.admin-header__theme-option-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.admin-header__theme-option-label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-header__theme-option-check {
|
||||
flex-shrink: 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.admin-header__theme-option:hover {
|
||||
background: var(--color-border-light);
|
||||
}
|
||||
|
||||
.admin-header__theme-option--active {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.admin-header__theme-option--active .admin-header__theme-option-icon {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.admin-header {
|
||||
padding: 0 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.admin-header__icon-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.admin-header__icon-btn {
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.admin-header__panel {
|
||||
position: fixed;
|
||||
top: var(--header-height);
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
width: auto;
|
||||
max-height: calc(100dvh - var(--header-height) - 16px);
|
||||
}
|
||||
|
||||
.admin-header__panel-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.admin-header__panel-action {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.admin-header__user-info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.admin-header__user {
|
||||
border-right: none;
|
||||
padding-right: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,154 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ADMIN_NAV_ITEMS, isNavActive } from '@/utils/navigation'
|
||||
import AdminIcon from '@/components/icons/AdminIcon.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
defineProps<{
|
||||
open: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
function navigate(path: string) {
|
||||
router.push(path)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function logout() {
|
||||
auth.logout()
|
||||
router.push('/login')
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="admin-drawer-backdrop"
|
||||
:class="{ open }"
|
||||
role="presentation"
|
||||
@click="emit('close')"
|
||||
/>
|
||||
<aside
|
||||
class="admin-drawer"
|
||||
:class="{ open }"
|
||||
aria-label="منوی ناوبری"
|
||||
:aria-hidden="!open"
|
||||
>
|
||||
<div class="admin-drawer__brand">
|
||||
<strong>پارچه</strong>
|
||||
<span>پنل مدیریت</span>
|
||||
<button type="button" class="admin-drawer__close" aria-label="بستن منو" @click="emit('close')">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="admin-drawer__nav">
|
||||
<button
|
||||
v-for="item in ADMIN_NAV_ITEMS"
|
||||
:key="item.name"
|
||||
type="button"
|
||||
class="admin-drawer__item"
|
||||
:class="{ active: isNavActive(item.path, route.path) }"
|
||||
@click="navigate(item.path)"
|
||||
>
|
||||
<AdminIcon :name="item.icon" :size="20" />
|
||||
<span>{{ item.label }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<button type="button" class="admin-drawer__logout" @click="logout">
|
||||
<AdminIcon name="logout" :size="20" />
|
||||
<span>خروج</span>
|
||||
</button>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-drawer__brand {
|
||||
position: relative;
|
||||
padding: 20px 16px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.admin-drawer__brand strong {
|
||||
display: block;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.admin-drawer__brand span {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.admin-drawer__close {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 12px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #fff;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.admin-drawer__nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.admin-drawer__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.admin-drawer__item.active {
|
||||
background: var(--color-sidebar-active);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-drawer__logout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 8px 10px 16px;
|
||||
padding: 12px 14px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: #f87171;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.admin-drawer__logout:hover {
|
||||
background: rgba(248, 113, 113, 0.12);
|
||||
color: #fca5a5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ADMIN_NAV_ITEMS, isNavActive } from '@/utils/navigation'
|
||||
import AdminIcon from '@/components/icons/AdminIcon.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
function navigate(path: string) {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
function logout() {
|
||||
auth.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="admin-sidebar" aria-label="ناوبری پنل مدیریت">
|
||||
<div class="admin-sidebar__brand">
|
||||
<strong class="admin-sidebar__title">پارچه</strong>
|
||||
<span class="admin-sidebar__subtitle">پنل مدیریت</span>
|
||||
</div>
|
||||
|
||||
<nav class="admin-sidebar__nav">
|
||||
<button
|
||||
v-for="item in ADMIN_NAV_ITEMS"
|
||||
:key="item.name"
|
||||
type="button"
|
||||
class="admin-sidebar__item"
|
||||
:class="{ active: isNavActive(item.path, route.path) }"
|
||||
@click="navigate(item.path)"
|
||||
>
|
||||
<AdminIcon :name="item.icon" :size="20" />
|
||||
<span>{{ item.label }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<button type="button" class="admin-sidebar__logout" @click="logout">
|
||||
<AdminIcon name="logout" :size="20" />
|
||||
<span>خروج</span>
|
||||
</button>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-sidebar {
|
||||
width: var(--sidebar-width);
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
background: var(--color-sidebar-bg);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-sidebar__brand {
|
||||
padding: 24px 20px 20px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.admin-sidebar__title {
|
||||
display: block;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.admin-sidebar__subtitle {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.admin-sidebar__nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.admin-sidebar__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
transition: background var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
|
||||
.admin-sidebar__item:focus-visible {
|
||||
outline: 2px solid rgba(255, 255, 255, 0.45);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.admin-sidebar__item:hover {
|
||||
background: var(--color-sidebar-hover);
|
||||
}
|
||||
|
||||
.admin-sidebar__item.active {
|
||||
background: var(--color-sidebar-active);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-sidebar__logout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 8px 10px 16px;
|
||||
padding: 10px 14px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: #f87171;
|
||||
font-size: 0.8125rem;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.admin-sidebar__logout:hover {
|
||||
background: rgba(248, 113, 113, 0.12);
|
||||
color: #fca5a5;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user