Refactor layout components for improved readability and consistency

This commit is contained in:
skytek_xinliang
2026-03-27 13:57:44 +08:00
parent 24f86c3fb5
commit 6e38211382
24 changed files with 235 additions and 190 deletions
+4 -4
View File
@@ -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>
+18 -9
View File
@@ -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'
+152 -90
View File
@@ -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,38 +295,36 @@ const props = withDefaults(defineProps<Props>(), {
sidebarToggleLabel: '切換側欄', sidebarToggleLabel: '切換側欄',
favoriteHeaderLabel: '我的最愛', favoriteHeaderLabel: '我的最愛',
favoriteItems: () => [], favoriteItems: () => [],
menuItems: () => [ menuItems: () => [{ title: '首頁', path: '/' }],
{ title: '首頁', path: '/' },
],
userProfile: () => ({ userProfile: () => ({
name: '王小明', name: '王小明',
role: '資訊工程系 - 學生', role: '資訊工程系 - 學生',
avatarText: '王', avatarText: '王',
}), }),
searchConfig: () => ({ searchConfig: () => ({
placeholder: '搜尋功能名稱... (試試「成績」、「選課」、「請假」)', placeholder: '搜尋功能名稱... (試試「成績」、「選課」、「請假」)',
label: '搜尋', label: '搜尋',
}), }),
toolbarActions: () => ({ toolbarActions: () => ({
notificationsLabel: '通知', notificationsLabel: '通知',
messagesLabel: '訊息', messagesLabel: '訊息',
helpLabel: '說明', helpLabel: '說明',
settingsLabel: '設定', settingsLabel: '設定',
}), }),
toolbarCounts: () => ({ toolbarCounts: () => ({
notifications: 0, notifications: 0,
messages: 0, messages: 0,
}), }),
favoritesConfig: () => ({ favoritesConfig: () => ({
label: '常用', label: '常用',
addLabel: '新增常用', addLabel: '新增常用',
showAdd: false, showAdd: false,
}), }),
breadcrumbConfig: () => ({ breadcrumbConfig: () => ({
homeLabel: '首頁', homeLabel: '首頁',
homeDisabled: true, homeDisabled: true,
homeIcon: mdiHome, homeIcon: mdiHome,
}), }),
breadcrumbItems: () => [], breadcrumbItems: () => [],
favoritesBarVisible: null, favoritesBarVisible: null,
breadcrumbBarVisible: null, breadcrumbBarVisible: null,
@@ -276,9 +338,9 @@ const props = withDefaults(defineProps<Props>(), {
showUserInfo: true, showUserInfo: true,
}), }),
drawerConfig: () => ({ drawerConfig: () => ({
width: 280, width: 280,
railWidth: 56, railWidth: 56,
}), }),
}) })
// Feature toggle: UI 區塊顯示 // Feature toggle: UI 區塊顯示
@@ -339,12 +401,12 @@ 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),
}) })
function toggleTheme () { function toggleTheme() {
const next = switchTheme() const next = switchTheme()
if (!next) return if (!next) return
emit('toggle-theme', next) emit('toggle-theme', next)
@@ -352,7 +414,7 @@ function toggleTheme () {
const emitLogout = () => emit('logout') const emitLogout = () => emit('logout')
function handleAction (type: AdminLayoutActionType) { function handleAction(type: AdminLayoutActionType) {
if (type === 'help') { if (type === 'help') {
helpWidgetVisible.value = true helpWidgetVisible.value = true
} }
@@ -360,41 +422,39 @@ function handleAction (type: AdminLayoutActionType) {
} }
// 以按鈕或 Enter 觸發搜尋,避免每個字都觸發 // 以按鈕或 Enter 觸發搜尋,避免每個字都觸發
function triggerSearch () { function triggerSearch() {
const keyword = searchValue.value const keyword = searchValue.value
emit('search', keyword) emit('search', keyword)
// 觸發後清空欄位,避免彈窗出現仍保留文字 // 觸發後清空欄位,避免彈窗出現仍保留文字
searchValue.value = '' searchValue.value = ''
} }
function emitAddFavorite () { function emitAddFavorite() {
emit('add-favorite') emit('add-favorite')
} }
function emitRemoveFavorite (item: AdminLayoutMenuItem) { function emitRemoveFavorite(item: AdminLayoutMenuItem) {
emit('remove-favorite', item) emit('remove-favorite', item)
} }
function onSelectFavorite (item: AdminLayoutMenuItem) { function onSelectFavorite(item: AdminLayoutMenuItem) {
handleSelectFavorite(item, handleSelect) handleSelectFavorite(item, handleSelect)
} }
function onMobileMenuClick (item: AdminLayoutMenuItem) { function onMobileMenuClick(item: AdminLayoutMenuItem) {
handleMobileMenuClick(item, handleSelect) handleMobileMenuClick(item, handleSelect)
} }
function handleSelect (item: AdminLayoutMenuItem) { function handleSelect(item: AdminLayoutMenuItem) {
emit('select', item) emit('select', item)
if (isMobile.value) drawer.value = false if (isMobile.value) drawer.value = false
} }
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) {
return level === mobileCurrentLevel.value ? 'primary' : 'secondary' return level === mobileCurrentLevel.value ? 'primary' : 'secondary'
} }
</script> </script>
@@ -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;
} }
/* 確保滾軸邊距 */ /* 確保滾軸邊距 */
+1 -1
View File
@@ -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<{
+20 -20
View File
@@ -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,12 +14,12 @@ 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
} }
export function useAdminLayoutState (options: UseAdminLayoutStateOptions) { export function useAdminLayoutState(options: UseAdminLayoutStateOptions) {
const drawer = ref(true) const drawer = ref(true)
const mobileFavoritesPanel = ref(false) const mobileFavoritesPanel = ref(false)
const mobileMenuPath = ref<AdminLayoutMenuItem[]>([]) const mobileMenuPath = ref<AdminLayoutMenuItem[]>([])
@@ -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,16 +77,17 @@ 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}`),
})) }))
) )
function resetMobilePanels () { function resetMobilePanels() {
mobileFavoritesPanel.value = false mobileFavoritesPanel.value = false
mobileMenuPath.value = [] mobileMenuPath.value = []
} }
function toggleSidebar () { function toggleSidebar() {
if (options.isMobile.value) { if (options.isMobile.value) {
drawer.value = !drawer.value drawer.value = !drawer.value
} else { } else {
@@ -99,17 +100,17 @@ export function useAdminLayoutState (options: UseAdminLayoutStateOptions) {
}) })
} }
function goToMobileLevel (level: number) { function goToMobileLevel(level: number) {
mobileFavoritesPanel.value = false mobileFavoritesPanel.value = false
mobileMenuPath.value = mobileMenuPath.value.slice(0, Math.max(0, level - 1)) mobileMenuPath.value = mobileMenuPath.value.slice(0, Math.max(0, level - 1))
} }
function openMobileFavoritesPanel () { function openMobileFavoritesPanel() {
mobileMenuPath.value = [] mobileMenuPath.value = []
mobileFavoritesPanel.value = true mobileFavoritesPanel.value = true
} }
function handleMobileMenuClick ( function handleMobileMenuClick(
item: AdminLayoutMenuItem, item: AdminLayoutMenuItem,
onSelect: (selectedItem: AdminLayoutMenuItem) => void onSelect: (selectedItem: AdminLayoutMenuItem) => void
) { ) {
@@ -121,7 +122,7 @@ export function useAdminLayoutState (options: UseAdminLayoutStateOptions) {
onSelect(item) onSelect(item)
} }
function handleSelectFavorite ( function handleSelectFavorite(
item: AdminLayoutMenuItem, item: AdminLayoutMenuItem,
onSelect: (selectedItem: AdminLayoutMenuItem) => void onSelect: (selectedItem: AdminLayoutMenuItem) => void
) { ) {
@@ -129,25 +130,24 @@ export function useAdminLayoutState (options: UseAdminLayoutStateOptions) {
mobileFavoritesPanel.value = false mobileFavoritesPanel.value = false
} }
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() {
isRail.value = false isRail.value = false
} }
let appBarObserver: ResizeObserver | null = null let appBarObserver: ResizeObserver | null = null
function resolveObservedElement () { function resolveObservedElement() {
const target = options.appBarRef.value as HTMLElement | { $el?: HTMLElement } | null const target = options.appBarRef.value as HTMLElement | { $el?: HTMLElement } | null
if (!target) return null if (!target) return null
if (target instanceof HTMLElement) return target if (target instanceof HTMLElement) return target
return target.$el ?? null return target.$el ?? null
} }
function updateAppBarHeight () { function updateAppBarHeight() {
const el = resolveObservedElement() const el = resolveObservedElement()
if (!el) return if (!el) return
appBarHeight.value = Math.round(el.getBoundingClientRect().height || 0) appBarHeight.value = Math.round(el.getBoundingClientRect().height || 0)
@@ -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 -1
View File
@@ -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'
+2 -22
View File
@@ -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,
-2
View File
@@ -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,
-2
View File
@@ -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,
+2 -4
View File
@@ -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,