接入前端登录权限底座
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<aside class="app-sidebar">
|
||||
<RouterLink
|
||||
class="brand"
|
||||
to="/reservation/orders"
|
||||
:to="brandTarget"
|
||||
>
|
||||
<span class="brand-mark" />
|
||||
<span>
|
||||
@@ -18,15 +18,15 @@
|
||||
>
|
||||
<RouterLink
|
||||
v-for="item in navItems"
|
||||
:key="item.to"
|
||||
:to="item.to"
|
||||
:key="item.menu_code"
|
||||
:to="item.route_path"
|
||||
class="app-nav__item"
|
||||
>
|
||||
<i
|
||||
:class="item.icon"
|
||||
:class="item.icon_key || 'pi pi-circle'"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{{ t(item.labelKey) }}</span>
|
||||
<span>{{ menuLabel(item) }}</span>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
</aside>
|
||||
@@ -35,10 +35,57 @@
|
||||
<header class="app-topbar">
|
||||
<div>
|
||||
<strong>{{ pageTitle }}</strong>
|
||||
<small>{{ reservationHotelId }}</small>
|
||||
<small>{{ currentHotelLabel }}</small>
|
||||
</div>
|
||||
|
||||
<div class="app-topbar__actions">
|
||||
<div
|
||||
v-if="authStore.isAuthenticated"
|
||||
class="auth-context"
|
||||
>
|
||||
<span class="user-chip">
|
||||
<i
|
||||
class="pi pi-user"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ currentUserLabel }}
|
||||
</span>
|
||||
|
||||
<label
|
||||
v-if="hotelSwitchable"
|
||||
class="hotel-select"
|
||||
:title="t('auth.currentHotel')"
|
||||
>
|
||||
<i
|
||||
class="pi pi-building"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<select
|
||||
:value="authStore.selectedHotelId ?? ''"
|
||||
@change="changeHotel"
|
||||
>
|
||||
<option
|
||||
v-for="hotel in authStore.hotels"
|
||||
:key="hotel.hotel_id"
|
||||
:value="hotel.hotel_id"
|
||||
>
|
||||
{{ hotel.hotel_name || hotel.hotel_id }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="hotel-chip"
|
||||
>
|
||||
<i
|
||||
class="pi pi-building"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{ currentHotelLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="health-pill"
|
||||
:class="`health-pill--${healthState}`"
|
||||
@@ -76,6 +123,19 @@
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="authStore.isAuthenticated"
|
||||
type="button"
|
||||
class="logout-button"
|
||||
:title="t('auth.logout')"
|
||||
@click="logout"
|
||||
>
|
||||
<i
|
||||
class="pi pi-sign-out"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -88,16 +148,19 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { reservationHotelId } from '@/config/reservationConfig'
|
||||
import type { AuthMenuResult } from '@/types/auth'
|
||||
import { fetchBackendHealth } from '@/services/healthService'
|
||||
import { useAppPreferencesStore, type SupportedLocale } from '@/stores/appPreferences'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { t, locale } = useI18n()
|
||||
const preferences = useAppPreferencesStore()
|
||||
const authStore = useAuthStore()
|
||||
const healthState = ref<'checking' | 'up' | 'down'>('checking')
|
||||
|
||||
const localeOptions: Array<{ value: SupportedLocale; label: string }> = [
|
||||
@@ -106,23 +169,20 @@ const localeOptions: Array<{ value: SupportedLocale; label: string }> = [
|
||||
{ value: 'th-TH', label: 'ไทย' },
|
||||
]
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
to: '/reservation/orders',
|
||||
icon: 'pi pi-list',
|
||||
labelKey: 'nav.orders',
|
||||
},
|
||||
{
|
||||
to: '/reservation/tasks',
|
||||
icon: 'pi pi-check-square',
|
||||
labelKey: 'nav.taskQueue',
|
||||
},
|
||||
{
|
||||
to: '/debug/eml-superagent',
|
||||
icon: 'pi pi-upload',
|
||||
labelKey: 'nav.debugEml',
|
||||
},
|
||||
]
|
||||
const menuLabelKeys: Record<string, string> = {
|
||||
RESERVATION_ORDERS: 'nav.orders',
|
||||
RESERVATION_TASKS: 'nav.taskQueue',
|
||||
DEBUG_EML_SUPERAGENT: 'nav.debugEml',
|
||||
}
|
||||
|
||||
const navItems = computed(() => authStore.visibleMenus)
|
||||
const brandTarget = computed(() => authStore.firstMenuPath ?? '/reservation/orders')
|
||||
const currentUserLabel = computed(() => authStore.user?.display_name || authStore.user?.username || '-')
|
||||
const currentHotelLabel = computed(() => {
|
||||
const hotel = authStore.selectedHotel
|
||||
return hotel?.hotel_name || hotel?.hotel_id || '-'
|
||||
})
|
||||
const hotelSwitchable = computed(() => authStore.hotels.length > 1 && authStore.hasPermission('HOTEL_SWITCH'))
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
const titleKey = typeof route.meta.titleKey === 'string' ? route.meta.titleKey : 'nav.orders'
|
||||
@@ -148,6 +208,23 @@ function setLocale(nextLocale: SupportedLocale): void {
|
||||
locale.value = nextLocale
|
||||
}
|
||||
|
||||
function menuLabel(item: AuthMenuResult): string {
|
||||
const labelKey = menuLabelKeys[item.menu_code]
|
||||
return labelKey ? t(labelKey) : item.menu_name
|
||||
}
|
||||
|
||||
function changeHotel(event: Event): void {
|
||||
const select = event.target
|
||||
if (select instanceof HTMLSelectElement) {
|
||||
authStore.setSelectedHotelId(select.value)
|
||||
}
|
||||
}
|
||||
|
||||
async function logout(): Promise<void> {
|
||||
await authStore.logout()
|
||||
await router.push('/login')
|
||||
}
|
||||
|
||||
async function checkHealth(): Promise<void> {
|
||||
healthState.value = 'checking'
|
||||
try {
|
||||
@@ -278,13 +355,49 @@ async function checkHealth(): Promise<void> {
|
||||
|
||||
.health-pill,
|
||||
.locale-switch,
|
||||
.theme-button {
|
||||
.theme-button,
|
||||
.logout-button,
|
||||
.user-chip,
|
||||
.hotel-chip,
|
||||
.hotel-select {
|
||||
min-height: 36px;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
}
|
||||
|
||||
.auth-context {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-chip,
|
||||
.hotel-chip,
|
||||
.hotel-select {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
color: var(--th-color-slate-700);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
padding: 0 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hotel-select {
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.hotel-select select {
|
||||
max-width: 150px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.health-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -314,7 +427,8 @@ async function checkHealth(): Promise<void> {
|
||||
}
|
||||
|
||||
.locale-switch button,
|
||||
.theme-button {
|
||||
.theme-button,
|
||||
.logout-button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--th-color-slate-700);
|
||||
@@ -334,7 +448,8 @@ async function checkHealth(): Promise<void> {
|
||||
color: var(--th-color-blue-600);
|
||||
}
|
||||
|
||||
.theme-button {
|
||||
.theme-button,
|
||||
.logout-button {
|
||||
width: 38px;
|
||||
}
|
||||
|
||||
@@ -394,6 +509,14 @@ async function checkHealth(): Promise<void> {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.auth-context {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.user-chip {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
padding: 18px;
|
||||
}
|
||||
@@ -438,7 +561,8 @@ async function checkHealth(): Promise<void> {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.theme-button {
|
||||
.theme-button,
|
||||
.logout-button {
|
||||
flex: 0 0 38px;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user