Refactor layout components for improved readability and consistency
This commit is contained in:
@@ -14,10 +14,10 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-tooltip :disabled="!searchToggleTooltipText" location="top" :text="searchToggleTooltipText">
|
<v-tooltip :disabled="!searchToggleTooltipText" location="top" :text="searchToggleTooltipText">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="showSearchToggle"
|
v-if="showSearchToggle"
|
||||||
v-bind="props"
|
v-bind="activatorProps"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
icon
|
icon
|
||||||
variant="text"
|
variant="text"
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
|
|
||||||
<v-tooltip :disabled="!refreshTooltipText" location="top" :text="refreshTooltipText">
|
<v-tooltip :disabled="!refreshTooltipText" location="top" :text="refreshTooltipText">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-btn v-bind="props" density="comfortable" icon variant="text" @click="$emit('refresh')">
|
<v-btn v-bind="activatorProps" density="comfortable" icon variant="text" @click="$emit('refresh')">
|
||||||
<v-icon :icon="mdiRefresh" />
|
<v-icon :icon="mdiRefresh" />
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,29 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="d-flex justify-end py-0 py-sm-2">
|
<div class="d-flex justify-end py-0 py-sm-2">
|
||||||
<v-btn
|
<v-btn
|
||||||
class="d-none d-md-block" color="grey-darken-1" :icon="mdiPaletteOutline" size="small" variant="text"
|
class="d-none d-md-block"
|
||||||
|
color="grey-darken-1"
|
||||||
|
:icon="mdiPaletteOutline"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
@click="toggleTheme"></v-btn>
|
@click="toggleTheme"></v-btn>
|
||||||
<!-- <v-btn :icon="mdiDockWindow" variant="text" size="small" color="grey-darken-1" @click="handleToggleLayout"></v-btn> -->
|
|
||||||
<v-menu location="bottom end">
|
<v-menu location="bottom end">
|
||||||
<template #activator="{ props: menuActivatorProps }">
|
<template #activator="{ props: menuActivatorProps }">
|
||||||
<v-btn
|
<v-btn
|
||||||
v-bind="menuActivatorProps" color="grey-darken-1" :icon="mdiTranslate" size="small"
|
v-bind="menuActivatorProps"
|
||||||
variant="text"></v-btn>
|
color="grey-darken-1"
|
||||||
|
:icon="mdiTranslate"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
></v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-list density="compact">
|
<v-list density="compact">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-for="locale in localeOptions" :key="locale" :active="locale === props.locale"
|
v-for="localeOption in localeOptions"
|
||||||
@click="handleSelectLocale(locale)">
|
:key="localeOption"
|
||||||
<v-list-item-title>{{ localeLabels[locale] ?? locale }}</v-list-item-title>
|
:active="localeOption === props.locale"
|
||||||
|
@click="handleSelectLocale(localeOption)"
|
||||||
|
>
|
||||||
|
<v-list-item-title>{{ localeLabels[localeOption] ?? localeOption }}</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
<!-- <v-btn :icon="mdiWeatherNight" variant="text" size="small" color="grey-darken-1"></v-btn> -->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { mdiDockWindow, mdiPaletteOutline, mdiTranslate, mdiWeatherNight } from '@mdi/js'
|
import { mdiPaletteOutline, mdiTranslate } from '@mdi/js'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useTheme } from 'vuetify'
|
import { useTheme } from 'vuetify'
|
||||||
import { getNextThemeName } from '@/utils/theme'
|
import { getNextThemeName } from '@/utils/theme'
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-app v-bind="$attrs" class="sk-admin-layout">
|
<v-app v-bind="$attrs" class="sk-admin-layout">
|
||||||
<v-navigation-drawer
|
<v-navigation-drawer
|
||||||
v-model="drawer" class="sk-admin-drawer" color="surface" :rail="isRail"
|
v-model="drawer"
|
||||||
:rail-width="railWidth" :temporary="isMobile" :width="drawerWidth">
|
class="sk-admin-drawer"
|
||||||
|
color="surface"
|
||||||
|
:rail="isRail"
|
||||||
|
:rail-width="railWidth"
|
||||||
|
:temporary="isMobile"
|
||||||
|
:width="drawerWidth"
|
||||||
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
|
|
||||||
<!-- Sidebar Title -->
|
<!-- Sidebar Title -->
|
||||||
<v-card class="sidebar-header d-flex align-center pa-0 pl-3 py-2" flat>
|
<v-card class="sidebar-header d-flex align-center pa-0 pl-3 py-2" flat>
|
||||||
<v-btn
|
<v-btn
|
||||||
:aria-label="sidebarToggleLabel" color="grey" :icon="isRail ? mdiMenuOpen : mdiMenu" size="32"
|
:aria-label="sidebarToggleLabel"
|
||||||
variant="text" @click="toggleSidebar" />
|
color="grey"
|
||||||
|
:icon="isRail ? mdiMenuOpen : mdiMenu"
|
||||||
|
size="32"
|
||||||
|
variant="text"
|
||||||
|
@click="toggleSidebar"
|
||||||
|
/>
|
||||||
<v-card-text v-if="!isRail" class="sidebar-title flex-grow-1 py-0">
|
<v-card-text v-if="!isRail" class="sidebar-title flex-grow-1 py-0">
|
||||||
<slot name="title">
|
<slot name="title">
|
||||||
<div class="text-subtitle-1 font-weight-bold text-on-surface">
|
<div class="text-subtitle-1 font-weight-bold text-on-surface">
|
||||||
@@ -24,7 +34,11 @@ v-model="drawer" class="sk-admin-drawer" color="surface" :rail="isRail"
|
|||||||
<v-divider />
|
<v-divider />
|
||||||
|
|
||||||
<!-- User Info -->
|
<!-- User Info -->
|
||||||
<v-card v-if="features.showUserInfo" class="user-info d-flex align-center pa-0 pl-3 py-2" flat>
|
<v-card
|
||||||
|
v-if="features.showUserInfo"
|
||||||
|
class="user-info d-flex align-center pa-0 pl-3 py-2"
|
||||||
|
flat
|
||||||
|
>
|
||||||
<v-avatar class="user-avatar" color="primary" size="32" variant="tonal">
|
<v-avatar class="user-avatar" color="primary" size="32" variant="tonal">
|
||||||
<span class="text-subtitle-2 font-weight-bold">{{ userProfile.avatarText }}</span>
|
<span class="text-subtitle-2 font-weight-bold">{{ userProfile.avatarText }}</span>
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
@@ -39,17 +53,34 @@ v-model="drawer" class="sk-admin-drawer" color="surface" :rail="isRail"
|
|||||||
</v-card>
|
</v-card>
|
||||||
<v-divider />
|
<v-divider />
|
||||||
|
|
||||||
<v-sheet v-if="isMobile" class="mobile-menu-subheader d-flex flex-column align-stretch ga-2 px-3 py-2">
|
<v-sheet
|
||||||
|
v-if="isMobile"
|
||||||
|
class="mobile-menu-subheader d-flex flex-column align-stretch ga-2 px-3 py-2"
|
||||||
|
>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="features.showFavorites" class="justify-start text-none"
|
v-if="features.showFavorites"
|
||||||
color="primary" rounded="pill" size="small" :variant="mobileFavoritesPanel ? 'flat' : 'outlined'"
|
class="justify-start text-none"
|
||||||
@click="openMobileFavoritesPanel">
|
color="primary"
|
||||||
<span class="text-on-secondary text-caption font-weight-medium">{{ favoritesConfig.label }}</span>
|
rounded="pill"
|
||||||
|
size="small"
|
||||||
|
:variant="mobileFavoritesPanel ? 'flat' : 'outlined'"
|
||||||
|
@click="openMobileFavoritesPanel"
|
||||||
|
>
|
||||||
|
<span class="text-on-secondary text-caption font-weight-medium">{{
|
||||||
|
favoritesConfig.label
|
||||||
|
}}</span>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-for="step in mobileMenuLevels" :key="`mobile-level-${step.level}`"
|
v-for="step in mobileMenuLevels"
|
||||||
block class="justify-start text-none" :color="getMobileMenuBtnColor(step.level)"
|
:key="`mobile-level-${step.level}`"
|
||||||
rounded="pill" size="small" :variant="getMobileMenuBtnVariant(step.level)" @click="goToMobileLevel(step.level)">
|
block
|
||||||
|
class="justify-start text-none"
|
||||||
|
:color="getMobileMenuBtnColor(step.level)"
|
||||||
|
rounded="pill"
|
||||||
|
size="small"
|
||||||
|
:variant="getMobileMenuBtnVariant(step.level)"
|
||||||
|
@click="goToMobileLevel(step.level)"
|
||||||
|
>
|
||||||
{{ step.title }}
|
{{ step.title }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
@@ -58,47 +89,76 @@ v-for="step in mobileMenuLevels" :key="`mobile-level-${step.level}`"
|
|||||||
<!-- 桌面板選單 -->
|
<!-- 桌面板選單 -->
|
||||||
<template v-if="!isMobile">
|
<template v-if="!isMobile">
|
||||||
<SkAdminDrawerDesktopMenu
|
<SkAdminDrawerDesktopMenu
|
||||||
v-model:opened="opened" :is-shrink="isRail" :menu-items="menuItems"
|
v-model:opened="opened"
|
||||||
@select="handleSelect" @unshrink="handleUnshrink" />
|
:is-shrink="isRail"
|
||||||
|
:menu-items="menuItems"
|
||||||
|
@select="handleSelect"
|
||||||
|
@unshrink="handleUnshrink"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 行動版選單 -->
|
<!-- 行動版選單 -->
|
||||||
<template v-if="isMobile">
|
<template v-if="isMobile">
|
||||||
<SkAdminDrawerMobileFavoritesPanel
|
<SkAdminDrawerMobileFavoritesPanel
|
||||||
v-if="features.showFavorites && mobileFavoritesPanel"
|
v-if="features.showFavorites && mobileFavoritesPanel"
|
||||||
:favorite-items="favoriteItems" @select="onSelectFavorite" />
|
:favorite-items="favoriteItems"
|
||||||
|
@select="onSelectFavorite"
|
||||||
|
/>
|
||||||
<SkAdminDrawerMobileMenuPanel
|
<SkAdminDrawerMobileMenuPanel
|
||||||
v-else :mobile-current-items="mobileCurrentItems"
|
v-else
|
||||||
@item-click="onMobileMenuClick" />
|
:mobile-current-items="mobileCurrentItems"
|
||||||
|
@item-click="onMobileMenuClick"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
|
|
||||||
<v-app-bar ref="appBarRef" class="" height="auto">
|
<v-app-bar ref="appBarRef" class="" height="auto">
|
||||||
<v-row class="flex-column" no-gutters>
|
<v-row class="flex-column" no-gutters>
|
||||||
|
|
||||||
<SkAdminAppBarTopCol
|
<SkAdminAppBarTopCol
|
||||||
:features="features" :is-mobile="isMobile" :logout-label="logoutLabel"
|
:features="features"
|
||||||
:search-config="searchConfig" :search-value="searchValue" :show-breadcrumb-bar="showBreadcrumbBar"
|
:is-mobile="isMobile"
|
||||||
:show-favorites-bar="showFavoritesBar" :theme-toggle-label="themeToggleLabel" :toolbar-actions="toolbarActions"
|
:logout-label="logoutLabel"
|
||||||
:toolbar-counts="toolbarCounts" @action="handleAction"
|
:search-config="searchConfig"
|
||||||
@logout="emitLogout" @search="triggerSearch" @toggle-drawer="drawer = !drawer"
|
:search-value="searchValue"
|
||||||
@toggle-theme="toggleTheme" @update:search-value="searchValue = $event" @update:show-breadcrumb-bar="showBreadcrumbBar = $event"
|
:show-breadcrumb-bar="showBreadcrumbBar"
|
||||||
@update:show-favorites-bar="showFavoritesBar = $event">
|
:show-favorites-bar="showFavoritesBar"
|
||||||
|
:theme-toggle-label="themeToggleLabel"
|
||||||
|
:toolbar-actions="toolbarActions"
|
||||||
|
:toolbar-counts="toolbarCounts"
|
||||||
|
@action="handleAction"
|
||||||
|
@logout="emitLogout"
|
||||||
|
@search="triggerSearch"
|
||||||
|
@toggle-drawer="drawer = !drawer"
|
||||||
|
@toggle-theme="toggleTheme"
|
||||||
|
@update:search-value="searchValue = $event"
|
||||||
|
@update:show-breadcrumb-bar="showBreadcrumbBar = $event"
|
||||||
|
@update:show-favorites-bar="showFavoritesBar = $event"
|
||||||
|
>
|
||||||
<template v-if="$slots.actions" #actions>
|
<template v-if="$slots.actions" #actions>
|
||||||
<slot name="actions"></slot>
|
<slot name="actions"></slot>
|
||||||
</template>
|
</template>
|
||||||
</SkAdminAppBarTopCol>
|
</SkAdminAppBarTopCol>
|
||||||
|
|
||||||
<SkAdminAppBarFavoritesCol
|
<SkAdminAppBarFavoritesCol
|
||||||
:favorite-items="favoriteItems" :favorites-config="favoritesConfig" :features="features"
|
:favorite-items="favoriteItems"
|
||||||
:is-mobile="isMobile" :show-favorites-bar="showFavoritesBar" @add-favorite="emitAddFavorite"
|
:favorites-config="favoritesConfig"
|
||||||
@remove-favorite="emitRemoveFavorite" @select="handleSelect"
|
:features="features"
|
||||||
@toggle-favorites-bar="toggleFavoritesBar" />
|
:is-mobile="isMobile"
|
||||||
|
:show-favorites-bar="showFavoritesBar"
|
||||||
|
@add-favorite="emitAddFavorite"
|
||||||
|
@remove-favorite="emitRemoveFavorite"
|
||||||
|
@select="handleSelect"
|
||||||
|
@toggle-favorites-bar="toggleFavoritesBar"
|
||||||
|
/>
|
||||||
|
|
||||||
<SkAdminAppBarBreadcrumbCol
|
<SkAdminAppBarBreadcrumbCol
|
||||||
:breadcrumb-items="breadcrumbItems" :features="features" :is-mobile="isMobile"
|
:breadcrumb-items="breadcrumbItems"
|
||||||
:show-breadcrumb-bar="showBreadcrumbBar" :show-favorites-bar="showFavoritesBar"
|
:features="features"
|
||||||
@toggle-favorites-bar="toggleFavoritesBar">
|
:is-mobile="isMobile"
|
||||||
|
:show-breadcrumb-bar="showBreadcrumbBar"
|
||||||
|
:show-favorites-bar="showFavoritesBar"
|
||||||
|
@toggle-favorites-bar="toggleFavoritesBar"
|
||||||
|
>
|
||||||
<template v-if="$slots['breadcrumb-actions']" #breadcrumb-actions>
|
<template v-if="$slots['breadcrumb-actions']" #breadcrumb-actions>
|
||||||
<slot name="breadcrumb-actions"></slot>
|
<slot name="breadcrumb-actions"></slot>
|
||||||
</template>
|
</template>
|
||||||
@@ -125,8 +185,12 @@ v-else :mobile-current-items="mobileCurrentItems"
|
|||||||
<v-card-title class="text-subtitle-2">操作說明</v-card-title>
|
<v-card-title class="text-subtitle-2">操作說明</v-card-title>
|
||||||
<template #append>
|
<template #append>
|
||||||
<v-btn
|
<v-btn
|
||||||
aria-label="關閉說明" :icon="mdiClose" size="small" variant="text"
|
aria-label="關閉說明"
|
||||||
@click="helpWidgetVisible = false" />
|
:icon="mdiClose"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="helpWidgetVisible = false"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</v-card-item>
|
</v-card-item>
|
||||||
<v-divider />
|
<v-divider />
|
||||||
@@ -146,11 +210,6 @@ aria-label="關閉說明" :icon="mdiClose" size="small" variant="text"
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { mdiClose, mdiHelpCircleOutline, mdiHome, mdiMenu, mdiMenuOpen } from '@mdi/js'
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import { useAdminLayoutState } from '@/composables/layout/useAdminLayoutState'
|
|
||||||
import { useDisplay } from 'vuetify'
|
|
||||||
import { useThemeToggle } from '@/composables/layout/useThemeToggle'
|
|
||||||
import type {
|
import type {
|
||||||
AdminLayoutActionType,
|
AdminLayoutActionType,
|
||||||
AdminLayoutBreadcrumbConfig,
|
AdminLayoutBreadcrumbConfig,
|
||||||
@@ -164,6 +223,11 @@ import type {
|
|||||||
AdminLayoutToolbarCounts,
|
AdminLayoutToolbarCounts,
|
||||||
AdminLayoutUserProfile,
|
AdminLayoutUserProfile,
|
||||||
} from './sk-admin-layout/types'
|
} from './sk-admin-layout/types'
|
||||||
|
import { mdiClose, mdiHelpCircleOutline, mdiHome, mdiMenu, mdiMenuOpen } from '@mdi/js'
|
||||||
|
import { computed, ref, toRef } from 'vue'
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
|
import { useAdminLayoutState } from '@/composables/layout/useAdminLayoutState'
|
||||||
|
import { useThemeToggle } from '@/composables/layout/useThemeToggle'
|
||||||
import SkAdminAppBarBreadcrumbCol from './sk-admin-layout/SkAdminAppBarBreadcrumbCol.vue'
|
import SkAdminAppBarBreadcrumbCol from './sk-admin-layout/SkAdminAppBarBreadcrumbCol.vue'
|
||||||
import SkAdminAppBarFavoritesCol from './sk-admin-layout/SkAdminAppBarFavoritesCol.vue'
|
import SkAdminAppBarFavoritesCol from './sk-admin-layout/SkAdminAppBarFavoritesCol.vue'
|
||||||
import SkAdminAppBarTopCol from './sk-admin-layout/SkAdminAppBarTopCol.vue'
|
import SkAdminAppBarTopCol from './sk-admin-layout/SkAdminAppBarTopCol.vue'
|
||||||
@@ -176,7 +240,7 @@ const emit = defineEmits<{
|
|||||||
select: [item: AdminLayoutMenuItem]
|
select: [item: AdminLayoutMenuItem]
|
||||||
search: [keyword: string]
|
search: [keyword: string]
|
||||||
action: [type: AdminLayoutActionType]
|
action: [type: AdminLayoutActionType]
|
||||||
'toggle-sidebar': [payload: { drawer: boolean, rail: boolean }]
|
'toggle-sidebar': [payload: { drawer: boolean; rail: boolean }]
|
||||||
'toggle-theme': [themeName: string]
|
'toggle-theme': [themeName: string]
|
||||||
'add-favorite': []
|
'add-favorite': []
|
||||||
'remove-favorite': [item: AdminLayoutMenuItem]
|
'remove-favorite': [item: AdminLayoutMenuItem]
|
||||||
@@ -231,9 +295,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
sidebarToggleLabel: '切換側欄',
|
sidebarToggleLabel: '切換側欄',
|
||||||
favoriteHeaderLabel: '我的最愛',
|
favoriteHeaderLabel: '我的最愛',
|
||||||
favoriteItems: () => [],
|
favoriteItems: () => [],
|
||||||
menuItems: () => [
|
menuItems: () => [{ title: '首頁', path: '/' }],
|
||||||
{ title: '首頁', path: '/' },
|
|
||||||
],
|
|
||||||
userProfile: () => ({
|
userProfile: () => ({
|
||||||
name: '王小明',
|
name: '王小明',
|
||||||
role: '資訊工程系 - 學生',
|
role: '資訊工程系 - 學生',
|
||||||
@@ -339,7 +401,7 @@ const {
|
|||||||
emitUpdateIsRail: (value) => emit('update:isRail', value),
|
emitUpdateIsRail: (value) => emit('update:isRail', value),
|
||||||
favoritesBarVisible: props.favoritesBarVisible,
|
favoritesBarVisible: props.favoritesBarVisible,
|
||||||
isMobile,
|
isMobile,
|
||||||
isRail: props.isRail,
|
isRail: toRef(props, 'isRail'), // 傳 Ref 確保 composable getter 能隨 prop 更新
|
||||||
menuItems: props.menuItems,
|
menuItems: props.menuItems,
|
||||||
onToggleSidebar: (payload) => emit('toggle-sidebar', payload),
|
onToggleSidebar: (payload) => emit('toggle-sidebar', payload),
|
||||||
})
|
})
|
||||||
@@ -389,9 +451,7 @@ function handleSelect (item: AdminLayoutMenuItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getMobileMenuBtnVariant(level: number) {
|
function getMobileMenuBtnVariant(level: number) {
|
||||||
return !mobileFavoritesPanel.value && level === mobileCurrentLevel.value
|
return !mobileFavoritesPanel.value && level === mobileCurrentLevel.value ? 'flat' : 'outlined'
|
||||||
? 'flat'
|
|
||||||
: 'outlined'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMobileMenuBtnColor(level: number) {
|
function getMobileMenuBtnColor(level: number) {
|
||||||
@@ -433,8 +493,6 @@ function getMobileMenuBtnColor (level: number) {
|
|||||||
background: rgb(var(--v-theme-background));
|
background: rgb(var(--v-theme-background));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.search-input-wrapper {
|
.search-input-wrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -515,7 +573,9 @@ function getMobileMenuBtnColor (level: number) {
|
|||||||
/* 確保 v-list-group 的 activator 也有 hover 效果 */
|
/* 確保 v-list-group 的 activator 也有 hover 效果 */
|
||||||
:deep(.v-list-group > .v-list-item) {
|
:deep(.v-list-group > .v-list-item) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s ease, color 0.2s ease;
|
transition:
|
||||||
|
background-color 0.2s ease,
|
||||||
|
color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.v-list-group > .v-list-item:hover) {
|
:deep(.v-list-group > .v-list-item:hover) {
|
||||||
@@ -525,7 +585,9 @@ function getMobileMenuBtnColor (level: number) {
|
|||||||
|
|
||||||
/* 為所有 v-list-item 加上 transition */
|
/* 為所有 v-list-item 加上 transition */
|
||||||
:deep(.v-list-item) {
|
:deep(.v-list-item) {
|
||||||
transition: background-color 0.2s ease, color 0.2s ease;
|
transition:
|
||||||
|
background-color 0.2s ease,
|
||||||
|
color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 確保滾軸邊距 */
|
/* 確保滾軸邊距 */
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
|
||||||
import type { AdminLayoutFavoritesConfig, AdminLayoutMenuItem } from './sk-admin-layout/types'
|
import type { AdminLayoutFavoritesConfig, AdminLayoutMenuItem } from './sk-admin-layout/types'
|
||||||
|
import { computed } from 'vue'
|
||||||
import SKAdminLayout from './SKAdminLayout.vue'
|
import SKAdminLayout from './SKAdminLayout.vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ v-if="features.showFavorites && !showFavoritesBar" class="mr-2" color="primary"
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { mdiChevronRight } from '@mdi/js'
|
|
||||||
import type { AdminLayoutBreadcrumbItem, AdminLayoutFeatures } from './types'
|
import type { AdminLayoutBreadcrumbItem, AdminLayoutFeatures } from './types'
|
||||||
|
import { mdiChevronRight } from '@mdi/js'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
features?: AdminLayoutFeatures
|
features?: AdminLayoutFeatures
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ v-if="favoritesConfig.showAdd" class="favorite-add" color="primary" size="small"
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { mdiEyeOff, mdiPlus } from '@mdi/js'
|
|
||||||
import type { AdminLayoutFavoritesConfig, AdminLayoutFeatures, AdminLayoutMenuItem } from './types'
|
import type { AdminLayoutFavoritesConfig, AdminLayoutFeatures, AdminLayoutMenuItem } from './types'
|
||||||
|
import { mdiEyeOff, mdiPlus } from '@mdi/js'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
features?: AdminLayoutFeatures
|
features?: AdminLayoutFeatures
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ v-model="searchValueModel" :aria-label="searchConfig.label" class="search-input"
|
|||||||
|
|
||||||
<!-- 通知 -->
|
<!-- 通知 -->
|
||||||
<v-tooltip location="bottom" :text="toolbarActions.notificationsLabel">
|
<v-tooltip location="bottom" :text="toolbarActions.notificationsLabel">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-btn
|
<v-btn
|
||||||
v-bind="props" :aria-label="toolbarActions.notificationsLabel" icon size="small" variant="text"
|
v-bind="activatorProps" :aria-label="toolbarActions.notificationsLabel" icon size="small" variant="text"
|
||||||
@click="emit('action', 'notifications')">
|
@click="emit('action', 'notifications')">
|
||||||
<v-badge
|
<v-badge
|
||||||
v-if="toolbarCounts.notifications" color="error" :content="toolbarCounts.notifications"
|
v-if="toolbarCounts.notifications" color="error" :content="toolbarCounts.notifications"
|
||||||
@@ -39,9 +39,9 @@ v-if="toolbarCounts.notifications" color="error" :content="toolbarCounts.notific
|
|||||||
|
|
||||||
<!-- 訊息 -->
|
<!-- 訊息 -->
|
||||||
<v-tooltip location="bottom" :text="toolbarActions.messagesLabel">
|
<v-tooltip location="bottom" :text="toolbarActions.messagesLabel">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-btn
|
<v-btn
|
||||||
v-bind="props" :aria-label="toolbarActions.messagesLabel" icon size="small" variant="text"
|
v-bind="activatorProps" :aria-label="toolbarActions.messagesLabel" icon size="small" variant="text"
|
||||||
@click="emit('action', 'messages')">
|
@click="emit('action', 'messages')">
|
||||||
<v-badge
|
<v-badge
|
||||||
v-if="toolbarCounts.messages" color="warning" :content="toolbarCounts.messages" offset-x="4"
|
v-if="toolbarCounts.messages" color="warning" :content="toolbarCounts.messages" offset-x="4"
|
||||||
@@ -55,9 +55,9 @@ v-if="toolbarCounts.messages" color="warning" :content="toolbarCounts.messages"
|
|||||||
|
|
||||||
<!-- 說明 -->
|
<!-- 說明 -->
|
||||||
<v-tooltip v-if="false" location="bottom" :text="toolbarActions.helpLabel">
|
<v-tooltip v-if="false" location="bottom" :text="toolbarActions.helpLabel">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-btn
|
<v-btn
|
||||||
v-bind="props" :aria-label="toolbarActions.helpLabel" icon size="small" variant="text"
|
v-bind="activatorProps" :aria-label="toolbarActions.helpLabel" icon size="small" variant="text"
|
||||||
@click="emit('action', 'help')">
|
@click="emit('action', 'help')">
|
||||||
<v-icon :icon="mdiHelp" />
|
<v-icon :icon="mdiHelp" />
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@@ -98,16 +98,16 @@ v-bind="{ ...menuProps, ...tooltipProps }" :aria-label="toolbarActions.settingsL
|
|||||||
|
|
||||||
<!-- 登出 -->
|
<!-- 登出 -->
|
||||||
<v-tooltip location="bottom" :text="logoutLabel">
|
<v-tooltip location="bottom" :text="logoutLabel">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-btn v-bind="props" :aria-label="logoutLabel" icon size="small" variant="text" @click="emit('logout')">
|
<v-btn v-bind="activatorProps" :aria-label="logoutLabel" icon size="small" variant="text" @click="emit('logout')">
|
||||||
<v-icon :icon="mdiLogout" />
|
<v-icon :icon="mdiLogout" />
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
|
|
||||||
<v-tooltip v-if="features.showThemeToggle" location="bottom" :text="themeToggleLabel">
|
<v-tooltip v-if="features.showThemeToggle" location="bottom" :text="themeToggleLabel">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-btn v-bind="props" :aria-label="themeToggleLabel" icon variant="text" @click="emit('toggle-theme')">
|
<v-btn v-bind="activatorProps" :aria-label="themeToggleLabel" icon variant="text" @click="emit('toggle-theme')">
|
||||||
<v-icon :icon="mdiPaletteOutline" />
|
<v-icon :icon="mdiPaletteOutline" />
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
@@ -118,9 +118,9 @@ v-bind="{ ...menuProps, ...tooltipProps }" :aria-label="toolbarActions.settingsL
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { AdminLayoutActionType, AdminLayoutFeatures, AdminLayoutSearchConfig, AdminLayoutToolbarActions, AdminLayoutToolbarCounts } from './types'
|
||||||
import { mdiBellOutline, mdiCogOutline, mdiHelp, mdiLogout, mdiMagnify, mdiMenu, mdiMessageTextOutline, mdiPaletteOutline } from '@mdi/js'
|
import { mdiBellOutline, mdiCogOutline, mdiHelp, mdiLogout, mdiMagnify, mdiMenu, mdiMessageTextOutline, mdiPaletteOutline } from '@mdi/js'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import type { AdminLayoutActionType, AdminLayoutFeatures, AdminLayoutSearchConfig, AdminLayoutToolbarActions, AdminLayoutToolbarCounts } from './types'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isMobile?: boolean
|
isMobile?: boolean
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<v-list v-model:opened="openedModel" color="primary" density="compact" prepend-gap="8">
|
<v-list v-model:opened="openedModel" color="primary" density="compact" prepend-gap="8">
|
||||||
<template v-for="item in menuItems" :key="item.path ?? item.title">
|
<template v-for="item in menuItems" :key="item.path ?? item.title">
|
||||||
<v-list-group v-if="item.subItems?.length" class="menu-group" :value="`menu:${item.path ?? item.title}`">
|
<v-list-group v-if="item.subItems?.length" class="menu-group" :value="`menu:${item.path ?? item.title}`">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-bind="isShrink ? undefined : props" :class="{ 'px-0': isShrink }"
|
v-bind="isShrink ? undefined : activatorProps" :class="{ 'px-0': isShrink }"
|
||||||
:link="isNavigable(item) && !!item.path" :to="isNavigable(item) ? item.path : undefined" @click="emitSelect(item)">
|
:link="isNavigable(item) && !!item.path" :to="isNavigable(item) ? item.path : undefined" @click="emitSelect(item)">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<v-icon v-if="item.icon" size="20" :icon="item.icon" />
|
<v-icon v-if="item.icon" size="20" :icon="item.icon" />
|
||||||
@@ -93,9 +93,9 @@ v-else :link="!!subItem.path" :prepend-icon="subItem.icon || mdiMenuRight" :to="
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { AdminLayoutMenuItem } from './types'
|
||||||
import { mdiCircleSmall, mdiMenuRight } from '@mdi/js'
|
import { mdiCircleSmall, mdiMenuRight } from '@mdi/js'
|
||||||
import { computed, watch } from 'vue'
|
import { computed, watch } from 'vue'
|
||||||
import type { AdminLayoutMenuItem } from './types'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
opened?: string[]
|
opened?: string[]
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ v-for="item in mobileCurrentItems" :key="item.path ?? item.title" class="mb-1" r
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { mdiArrowTopRight, mdiChevronRight } from '@mdi/js'
|
|
||||||
import type { AdminLayoutMenuItem } from './types'
|
import type { AdminLayoutMenuItem } from './types'
|
||||||
|
import { mdiArrowTopRight, mdiChevronRight } from '@mdi/js'
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
withDefaults(defineProps<{
|
||||||
mobileCurrentItems?: AdminLayoutMenuItem[]
|
mobileCurrentItems?: AdminLayoutMenuItem[]
|
||||||
|
|||||||
@@ -308,7 +308,6 @@ import { useEditableStudentGrid } from '@/composables/maintenance/useEditableStu
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
departments,
|
departments,
|
||||||
draftRows,
|
|
||||||
enrollYears,
|
enrollYears,
|
||||||
filteredStudents,
|
filteredStudents,
|
||||||
getDraftRow,
|
getDraftRow,
|
||||||
|
|||||||
@@ -65,8 +65,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
|
||||||
import type { SaveSummaryItem } from '@/composables/maintenance/useStudentMaintenanceForm'
|
import type { SaveSummaryItem } from '@/composables/maintenance/useStudentMaintenanceForm'
|
||||||
|
import { computed } from 'vue'
|
||||||
import CommonConfirmDialog from './CommonConfirmDialog.vue'
|
import CommonConfirmDialog from './CommonConfirmDialog.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<v-col cols="12" md="3">
|
<v-col cols="12" md="3">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
id="field-studentId"
|
id="field-studentId"
|
||||||
v-model="props.form.studentId"
|
v-model="form.studentId"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
:disabled="props.isFormLocked"
|
:disabled="props.isFormLocked"
|
||||||
:error-messages="props.fieldErrors.studentId"
|
:error-messages="props.fieldErrors.studentId"
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<v-col cols="12" md="3">
|
<v-col cols="12" md="3">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
id="field-name"
|
id="field-name"
|
||||||
v-model="props.form.name"
|
v-model="form.name"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
:disabled="props.isFormLocked"
|
:disabled="props.isFormLocked"
|
||||||
:error-messages="props.fieldErrors.name"
|
:error-messages="props.fieldErrors.name"
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<v-col cols="12" md="3">
|
<v-col cols="12" md="3">
|
||||||
<v-select
|
<v-select
|
||||||
id="field-department"
|
id="field-department"
|
||||||
v-model="props.form.department"
|
v-model="form.department"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
:disabled="props.isFormLocked"
|
:disabled="props.isFormLocked"
|
||||||
:error-messages="props.fieldErrors.department"
|
:error-messages="props.fieldErrors.department"
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
<v-col cols="12" md="3">
|
<v-col cols="12" md="3">
|
||||||
<v-select
|
<v-select
|
||||||
id="field-grade"
|
id="field-grade"
|
||||||
v-model="props.form.grade"
|
v-model="form.grade"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
:disabled="props.isFormLocked"
|
:disabled="props.isFormLocked"
|
||||||
:error-messages="props.fieldErrors.grade"
|
:error-messages="props.fieldErrors.grade"
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
<v-col cols="12" md="3">
|
<v-col cols="12" md="3">
|
||||||
<v-select
|
<v-select
|
||||||
id="field-enrollYear"
|
id="field-enrollYear"
|
||||||
v-model="props.form.enrollYear"
|
v-model="form.enrollYear"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
:disabled="props.isFormLocked"
|
:disabled="props.isFormLocked"
|
||||||
:error-messages="props.fieldErrors.enrollYear"
|
:error-messages="props.fieldErrors.enrollYear"
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
<v-col cols="12" md="3">
|
<v-col cols="12" md="3">
|
||||||
<v-select
|
<v-select
|
||||||
id="field-status"
|
id="field-status"
|
||||||
v-model="props.form.status"
|
v-model="form.status"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
:disabled="props.isFormLocked"
|
:disabled="props.isFormLocked"
|
||||||
:error-messages="props.fieldErrors.status"
|
:error-messages="props.fieldErrors.status"
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
<v-col cols="12" md="3">
|
<v-col cols="12" md="3">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
id="field-email"
|
id="field-email"
|
||||||
v-model="props.form.email"
|
v-model="form.email"
|
||||||
density="comfortable"
|
density="comfortable"
|
||||||
:disabled="props.isFormLocked"
|
:disabled="props.isFormLocked"
|
||||||
:error-messages="props.fieldErrors.email"
|
:error-messages="props.fieldErrors.email"
|
||||||
@@ -104,6 +104,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { StudentFormState } from '@/composables/maintenance/useStudentMaintenanceForm'
|
import type { StudentFormState } from '@/composables/maintenance/useStudentMaintenanceForm'
|
||||||
|
import { toRef } from 'vue'
|
||||||
|
|
||||||
interface GradeOption {
|
interface GradeOption {
|
||||||
title: string
|
title: string
|
||||||
@@ -121,6 +122,8 @@ const props = defineProps<{
|
|||||||
statuses: string[]
|
statuses: string[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const form = toRef(props, 'form')
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: 'clear-field', field: keyof StudentFormState): void
|
(event: 'clear-field', field: keyof StudentFormState): void
|
||||||
}>()
|
}>()
|
||||||
|
|||||||
@@ -165,8 +165,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { mdiArrowDown, mdiArrowUp, mdiBookOpenOutline, mdiChevronRight, mdiDeleteOutline, mdiPlus, mdiSchool, mdiSwapVertical } from '@mdi/js'
|
|
||||||
import type { CourseRecord, SemesterRecord } from '@/stores/semesters'
|
import type { CourseRecord, SemesterRecord } from '@/stores/semesters'
|
||||||
|
import { mdiArrowDown, mdiArrowUp, mdiBookOpenOutline, mdiChevronRight, mdiDeleteOutline, mdiPlus, mdiSchool, mdiSwapVertical } from '@mdi/js'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
type CourseSortKey = 'name' | 'credits' | 'score'
|
type CourseSortKey = 'name' | 'credits' | 'score'
|
||||||
|
|||||||
@@ -80,9 +80,8 @@ color="error" :disabled="isFormLocked" :icon="mdiDelete" size="small" variant="t
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { mdiChevronRight, mdiDelete, mdiPlus, mdiSchool } from '@mdi/js'
|
|
||||||
import type { CourseRecord, SemesterRecord } from '@/stores/semesters'
|
import type { CourseRecord, SemesterRecord } from '@/stores/semesters'
|
||||||
|
import { mdiChevronRight, mdiDelete, mdiPlus, mdiSchool } from '@mdi/js'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -48,8 +48,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { mdiChevronRight, mdiPlus, mdiSchool } from '@mdi/js'
|
|
||||||
import type { SemesterRecord } from '@/stores/semesters'
|
import type { SemesterRecord } from '@/stores/semesters'
|
||||||
|
import { mdiChevronRight, mdiPlus, mdiSchool } from '@mdi/js'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
semesters: SemesterRecord[]
|
semesters: SemesterRecord[]
|
||||||
|
|||||||
@@ -308,9 +308,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { mdiArrowLeft, mdiClose, mdiDelete, mdiPencil, mdiPlus } from '@mdi/js'
|
|
||||||
import type { SemesterRecord } from '@/stores/semesters'
|
import type { SemesterRecord } from '@/stores/semesters'
|
||||||
|
import { mdiArrowLeft, mdiClose, mdiDelete, mdiPencil, mdiPlus } from '@mdi/js'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { computed, onBeforeUnmount, onMounted, ref, watch, type Ref } from 'vue'
|
|
||||||
import type { AdminLayoutMenuItem } from '@/components/layouts/sk-admin-layout/types'
|
import type { AdminLayoutMenuItem } from '@/components/layouts/sk-admin-layout/types'
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref, type Ref, watch } from 'vue'
|
||||||
|
|
||||||
type ToggleSidebarPayload = {
|
type ToggleSidebarPayload = {
|
||||||
drawer: boolean
|
drawer: boolean
|
||||||
@@ -14,7 +14,7 @@ type UseAdminLayoutStateOptions = {
|
|||||||
emitUpdateIsRail: (value: boolean) => void
|
emitUpdateIsRail: (value: boolean) => void
|
||||||
favoritesBarVisible: boolean | null
|
favoritesBarVisible: boolean | null
|
||||||
isMobile: Ref<boolean>
|
isMobile: Ref<boolean>
|
||||||
isRail: boolean | null
|
isRail: Ref<boolean | null> // 必須為 Ref,確保父層 prop 更新時 getter 能即時反映
|
||||||
menuItems: AdminLayoutMenuItem[]
|
menuItems: AdminLayoutMenuItem[]
|
||||||
onToggleSidebar: (payload: ToggleSidebarPayload) => void
|
onToggleSidebar: (payload: ToggleSidebarPayload) => void
|
||||||
}
|
}
|
||||||
@@ -30,9 +30,9 @@ export function useAdminLayoutState (options: UseAdminLayoutStateOptions) {
|
|||||||
const appBarHeight = ref(0)
|
const appBarHeight = ref(0)
|
||||||
|
|
||||||
const isRail = computed({
|
const isRail = computed({
|
||||||
get: () => (options.isRail ?? localIsRail.value),
|
get: () => options.isRail.value ?? localIsRail.value,
|
||||||
set: (value: boolean) => {
|
set: (value: boolean) => {
|
||||||
if (options.isRail === null) {
|
if (options.isRail.value === null) {
|
||||||
localIsRail.value = value
|
localIsRail.value = value
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ export function useAdminLayoutState (options: UseAdminLayoutStateOptions) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const showFavoritesBar = computed({
|
const showFavoritesBar = computed({
|
||||||
get: () => (options.favoritesBarVisible ?? localFavoritesBarVisible.value),
|
get: () => options.favoritesBarVisible ?? localFavoritesBarVisible.value,
|
||||||
set: (value: boolean) => {
|
set: (value: boolean) => {
|
||||||
if (options.favoritesBarVisible === null) {
|
if (options.favoritesBarVisible === null) {
|
||||||
localFavoritesBarVisible.value = value
|
localFavoritesBarVisible.value = value
|
||||||
@@ -54,7 +54,7 @@ export function useAdminLayoutState (options: UseAdminLayoutStateOptions) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const showBreadcrumbBar = computed({
|
const showBreadcrumbBar = computed({
|
||||||
get: () => (options.breadcrumbBarVisible ?? localBreadcrumbBarVisible.value),
|
get: () => options.breadcrumbBarVisible ?? localBreadcrumbBarVisible.value,
|
||||||
set: (value: boolean) => {
|
set: (value: boolean) => {
|
||||||
if (options.breadcrumbBarVisible === null) {
|
if (options.breadcrumbBarVisible === null) {
|
||||||
localBreadcrumbBarVisible.value = value
|
localBreadcrumbBarVisible.value = value
|
||||||
@@ -77,7 +77,8 @@ export function useAdminLayoutState (options: UseAdminLayoutStateOptions) {
|
|||||||
const mobileMenuLevels = computed(() =>
|
const mobileMenuLevels = computed(() =>
|
||||||
Array.from({ length: mobileCurrentLevel.value }, (_, index) => ({
|
Array.from({ length: mobileCurrentLevel.value }, (_, index) => ({
|
||||||
level: index + 1,
|
level: index + 1,
|
||||||
title: index === 0 ? '主選單' : (mobileMenuPath.value[index - 1]?.title ?? `第${index + 1}層`),
|
title:
|
||||||
|
index === 0 ? '主選單' : (mobileMenuPath.value[index - 1]?.title ?? `第${index + 1}層`),
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -130,8 +131,7 @@ export function useAdminLayoutState (options: UseAdminLayoutStateOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleFavoritesBar(nextValue?: boolean) {
|
function toggleFavoritesBar(nextValue?: boolean) {
|
||||||
showFavoritesBar.value =
|
showFavoritesBar.value = typeof nextValue === 'boolean' ? nextValue : !showFavoritesBar.value
|
||||||
typeof nextValue === 'boolean' ? nextValue : !showFavoritesBar.value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUnshrink() {
|
function handleUnshrink() {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { computed, ref, type ComputedRef, type Ref } from 'vue'
|
import { computed, type ComputedRef, ref, type Ref } from 'vue'
|
||||||
|
|
||||||
type DialogMode = 'create' | 'edit' | 'view'
|
type DialogMode = 'create' | 'edit' | 'view'
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { computed, ref, type ComputedRef, type Ref } from 'vue'
|
|
||||||
import type { StudentRecord } from '@/stores/students'
|
import type { StudentRecord } from '@/stores/students'
|
||||||
|
import { computed, type ComputedRef, ref, type Ref } from 'vue'
|
||||||
|
|
||||||
interface GradeOption {
|
interface GradeOption {
|
||||||
title: string
|
title: string
|
||||||
@@ -225,4 +225,4 @@ export function useStudentMaintenanceForm (options: UseStudentMaintenanceFormOpt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { StudentFormState, SaveSummaryItem }
|
export type { SaveSummaryItem, StudentFormState }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { mdiHome } from '@mdi/js'
|
|
||||||
import type { LayoutMenuItem } from './menu'
|
import type { LayoutMenuItem } from './menu'
|
||||||
|
import { mdiHome } from '@mdi/js'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
|||||||
@@ -330,8 +330,8 @@ function handleDeleteSemester (id: number) {
|
|||||||
// 開始編輯子檔 (複製資料到暫存表單)
|
// 開始編輯子檔 (複製資料到暫存表單)
|
||||||
function startDetailEdit () {
|
function startDetailEdit () {
|
||||||
if (!selectedSemester.value) return
|
if (!selectedSemester.value) return
|
||||||
// Deep copy 以避免直接修改原始資料 (Vue 的響應式特性)
|
// 需要深拷貝來避免直接改到原始資料,且保留巢狀結構的語意
|
||||||
detailForm.value = JSON.parse(JSON.stringify(selectedSemester.value))
|
detailForm.value = structuredClone(selectedSemester.value)
|
||||||
isDetailEditing.value = true
|
isDetailEditing.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,24 +353,6 @@ function saveDetailEdit () {
|
|||||||
detailForm.value = null
|
detailForm.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 編輯模式:新增課程項目
|
|
||||||
function addCourseToDetail () {
|
|
||||||
if (!detailForm.value) return
|
|
||||||
detailForm.value.courses.push({
|
|
||||||
code: '',
|
|
||||||
name: '',
|
|
||||||
credits: 3,
|
|
||||||
score: 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 編輯模式:移除課程項目
|
|
||||||
function removeCourseFromDetail (index: number) {
|
|
||||||
if (!detailForm.value) return
|
|
||||||
detailForm.value.courses.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
errorSummary,
|
errorSummary,
|
||||||
fieldErrors,
|
fieldErrors,
|
||||||
@@ -412,7 +394,6 @@ const dialogSubtitle = computed(() => {
|
|||||||
// 載入/儲存時鎖定、檢視模式 readonly
|
// 載入/儲存時鎖定、檢視模式 readonly
|
||||||
const isFormLocked = computed(() => isLoading.value || isSaving.value)
|
const isFormLocked = computed(() => isLoading.value || isSaving.value)
|
||||||
const {
|
const {
|
||||||
closeDialog,
|
|
||||||
confirmClose,
|
confirmClose,
|
||||||
confirmCloseVisible,
|
confirmCloseVisible,
|
||||||
confirmDelete,
|
confirmDelete,
|
||||||
@@ -422,7 +403,6 @@ const {
|
|||||||
confirmSaveVisible,
|
confirmSaveVisible,
|
||||||
confirmSwitch,
|
confirmSwitch,
|
||||||
confirmSwitchVisible,
|
confirmSwitchVisible,
|
||||||
currentEditingRecord,
|
|
||||||
handleDialogVisibility,
|
handleDialogVisibility,
|
||||||
hasNextRecord,
|
hasNextRecord,
|
||||||
hasPrevRecord,
|
hasPrevRecord,
|
||||||
|
|||||||
@@ -474,7 +474,6 @@ const dialogSubtitle = computed(() => {
|
|||||||
// 載入/儲存時鎖定、檢視模式 readonly
|
// 載入/儲存時鎖定、檢視模式 readonly
|
||||||
const isFormLocked = computed(() => isLoading.value || isSaving.value)
|
const isFormLocked = computed(() => isLoading.value || isSaving.value)
|
||||||
const {
|
const {
|
||||||
closeDialog,
|
|
||||||
confirmClose,
|
confirmClose,
|
||||||
confirmCloseVisible,
|
confirmCloseVisible,
|
||||||
confirmDelete,
|
confirmDelete,
|
||||||
@@ -484,7 +483,6 @@ const {
|
|||||||
confirmSaveVisible,
|
confirmSaveVisible,
|
||||||
confirmSwitch,
|
confirmSwitch,
|
||||||
confirmSwitchVisible,
|
confirmSwitchVisible,
|
||||||
currentEditingRecord,
|
|
||||||
handleDialogVisibility,
|
handleDialogVisibility,
|
||||||
hasNextRecord,
|
hasNextRecord,
|
||||||
hasPrevRecord,
|
hasPrevRecord,
|
||||||
|
|||||||
@@ -484,7 +484,6 @@ const dialogSubtitle = computed(() => {
|
|||||||
// 載入/儲存時鎖定、檢視模式 readonly
|
// 載入/儲存時鎖定、檢視模式 readonly
|
||||||
const isFormLocked = computed(() => isLoading.value || isSaving.value)
|
const isFormLocked = computed(() => isLoading.value || isSaving.value)
|
||||||
const {
|
const {
|
||||||
closeDialog,
|
|
||||||
confirmClose,
|
confirmClose,
|
||||||
confirmCloseVisible,
|
confirmCloseVisible,
|
||||||
confirmDelete,
|
confirmDelete,
|
||||||
@@ -494,7 +493,6 @@ const {
|
|||||||
confirmSaveVisible,
|
confirmSaveVisible,
|
||||||
confirmSwitch,
|
confirmSwitch,
|
||||||
confirmSwitchVisible,
|
confirmSwitchVisible,
|
||||||
currentEditingRecord,
|
|
||||||
handleDialogVisibility,
|
handleDialogVisibility,
|
||||||
hasNextRecord,
|
hasNextRecord,
|
||||||
hasPrevRecord,
|
hasPrevRecord,
|
||||||
|
|||||||
@@ -402,11 +402,11 @@ import { mdiBroom, mdiDelete, mdiEye, mdiMagnify, mdiPencil } from '@mdi/js'
|
|||||||
import { computed, nextTick, ref } from 'vue'
|
import { computed, nextTick, ref } from 'vue'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import MaintenanceCrudDialogs from '@/components/maintenance/MaintenanceCrudDialogs.vue'
|
import MaintenanceCrudDialogs from '@/components/maintenance/MaintenanceCrudDialogs.vue'
|
||||||
import { useMaintenanceCrudFlow } from '@/composables/maintenance/useMaintenanceCrudFlow'
|
|
||||||
import { useStudentMaintenanceForm } from '@/composables/maintenance/useStudentMaintenanceForm'
|
|
||||||
import MntDialogCard from '@/components/maintenance/MntDialogCard.vue'
|
import MntDialogCard from '@/components/maintenance/MntDialogCard.vue'
|
||||||
import MntPageCards from '@/components/maintenance/MntPageCards.vue'
|
import MntPageCards from '@/components/maintenance/MntPageCards.vue'
|
||||||
import MntRecordNavToolbar from '@/components/maintenance/MntRecordNavToolbar.vue'
|
import MntRecordNavToolbar from '@/components/maintenance/MntRecordNavToolbar.vue'
|
||||||
|
import { useMaintenanceCrudFlow } from '@/composables/maintenance/useMaintenanceCrudFlow'
|
||||||
|
import { useStudentMaintenanceForm } from '@/composables/maintenance/useStudentMaintenanceForm'
|
||||||
import { type StudentRecord, useStudentStore } from '@/stores/students'
|
import { type StudentRecord, useStudentStore } from '@/stores/students'
|
||||||
|
|
||||||
// 下拉選項:系所/年級/入學年度/狀態
|
// 下拉選項:系所/年級/入學年度/狀態
|
||||||
@@ -525,7 +525,6 @@ const dialogSubtitle = computed(() => {
|
|||||||
// 載入/儲存時鎖定、檢視模式 readonly
|
// 載入/儲存時鎖定、檢視模式 readonly
|
||||||
const isFormLocked = computed(() => isLoading.value || isSaving.value)
|
const isFormLocked = computed(() => isLoading.value || isSaving.value)
|
||||||
const {
|
const {
|
||||||
closeDialog,
|
|
||||||
confirmClose,
|
confirmClose,
|
||||||
confirmCloseVisible,
|
confirmCloseVisible,
|
||||||
confirmDelete,
|
confirmDelete,
|
||||||
@@ -535,7 +534,6 @@ const {
|
|||||||
confirmSaveVisible,
|
confirmSaveVisible,
|
||||||
confirmSwitch,
|
confirmSwitch,
|
||||||
confirmSwitchVisible,
|
confirmSwitchVisible,
|
||||||
currentEditingRecord,
|
|
||||||
handleDialogVisibility,
|
handleDialogVisibility,
|
||||||
hasNextRecord,
|
hasNextRecord,
|
||||||
hasPrevRecord,
|
hasPrevRecord,
|
||||||
|
|||||||
Reference in New Issue
Block a user