接入前端登录权限底座

This commit is contained in:
andy
2026-07-09 20:42:16 +08:00
parent c190d5e3a8
commit e49b2ccc1d
27 changed files with 1745 additions and 68 deletions

View File

@@ -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;
}