refactor: replace common confirm dialogs with maintenance CRUD dialogs and streamline form handling in MasterDetailMntC.vue and SingleRecordMnt.vue

This commit is contained in:
skytek_xinliang
2026-03-26 16:01:20 +08:00
parent ec3fbace1a
commit 389ec56480
32 changed files with 2549 additions and 2763 deletions
+129 -256
View File
@@ -66,10 +66,10 @@ v-model:opened="opened" :is-shrink="isRail" :menu-items="menuItems"
<template v-if="isMobile">
<SkAdminDrawerMobileFavoritesPanel
v-if="features.showFavorites && mobileFavoritesPanel"
:favorite-items="favoriteItems" @select="handleSelectFavorite" />
:favorite-items="favoriteItems" @select="onSelectFavorite" />
<SkAdminDrawerMobileMenuPanel
v-else :mobile-current-items="mobileCurrentItems"
@item-click="handleMobileMenuClick" />
@item-click="onMobileMenuClick" />
</template>
</v-navigation-drawer>
@@ -145,11 +145,25 @@ aria-label="關閉說明" :icon="mdiClose" size="small" variant="text"
</v-app>
</template>
<script setup>
<script setup lang="ts">
import { mdiClose, mdiHelpCircleOutline, mdiHome, mdiMenu, mdiMenuOpen } from '@mdi/js'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useDisplay, useTheme } from 'vuetify'
import { getNextThemeName } from '@/utils/theme'
import { computed, ref } from 'vue'
import { useAdminLayoutState } from '@/composables/layout/useAdminLayoutState'
import { useDisplay } from 'vuetify'
import { useThemeToggle } from '@/composables/layout/useThemeToggle'
import type {
AdminLayoutActionType,
AdminLayoutBreadcrumbConfig,
AdminLayoutBreadcrumbItem,
AdminLayoutDrawerConfig,
AdminLayoutFavoritesConfig,
AdminLayoutFeatures,
AdminLayoutMenuItem,
AdminLayoutSearchConfig,
AdminLayoutToolbarActions,
AdminLayoutToolbarCounts,
AdminLayoutUserProfile,
} from './sk-admin-layout/types'
import SkAdminAppBarBreadcrumbCol from './sk-admin-layout/SkAdminAppBarBreadcrumbCol.vue'
import SkAdminAppBarFavoritesCol from './sk-admin-layout/SkAdminAppBarFavoritesCol.vue'
import SkAdminAppBarTopCol from './sk-admin-layout/SkAdminAppBarTopCol.vue'
@@ -157,21 +171,21 @@ import SkAdminDrawerDesktopMenu from './sk-admin-layout/SkAdminDrawerDesktopMenu
import SkAdminDrawerMobileFavoritesPanel from './sk-admin-layout/SkAdminDrawerMobileFavoritesPanel.vue'
import SkAdminDrawerMobileMenuPanel from './sk-admin-layout/SkAdminDrawerMobileMenuPanel.vue'
const emit = defineEmits([
'logout',
'select',
'search',
'action',
'toggle-sidebar',
'toggle-theme',
'add-favorite',
'remove-favorite',
'update:isRail',
'update:favoritesBarVisible',
'update:breadcrumbBarVisible',
])
const emit = defineEmits<{
logout: []
select: [item: AdminLayoutMenuItem]
search: [keyword: string]
action: [type: AdminLayoutActionType]
'toggle-sidebar': [payload: { drawer: boolean, rail: boolean }]
'toggle-theme': [themeName: string]
'add-favorite': []
'remove-favorite': [item: AdminLayoutMenuItem]
'update:isRail': [value: boolean]
'update:favoritesBarVisible': [value: boolean]
'update:breadcrumbBarVisible': [value: boolean]
}>()
const defaultFeatures = {
const defaultFeatures: AdminLayoutFeatures = {
showThemeToggle: false,
showFavorites: true,
showBreadcrumb: true,
@@ -180,111 +194,91 @@ const defaultFeatures = {
showUserInfo: true,
}
const defaultBreadcrumbConfig = {
const defaultBreadcrumbConfig: AdminLayoutBreadcrumbConfig = {
homeLabel: '首頁',
homeDisabled: true,
homeIcon: mdiHome,
}
const props = defineProps({
systemTitle: { type: String, default: '管理系統' },
systemSubtitle: { type: String, default: 'Campus System' },
themeToggleLabel: { type: String, default: '切換主題' },
logoutLabel: { type: String, default: '登出' },
sidebarToggleLabel: { type: String, default: '切換側欄' },
favoriteHeaderLabel: { type: String, default: '我的最愛' },
favoriteItems: {
type: Array,
default: () => [],
},
menuItems: {
type: Array,
default: () => [
interface Props {
systemTitle?: string
systemSubtitle?: string
themeToggleLabel?: string
logoutLabel?: string
sidebarToggleLabel?: string
favoriteHeaderLabel?: string
favoriteItems?: AdminLayoutMenuItem[]
menuItems?: AdminLayoutMenuItem[]
userProfile?: AdminLayoutUserProfile
searchConfig?: AdminLayoutSearchConfig
toolbarActions?: AdminLayoutToolbarActions
toolbarCounts?: AdminLayoutToolbarCounts
favoritesConfig?: AdminLayoutFavoritesConfig
breadcrumbConfig?: AdminLayoutBreadcrumbConfig
breadcrumbItems?: AdminLayoutBreadcrumbItem[]
favoritesBarVisible?: boolean | null
breadcrumbBarVisible?: boolean | null
isRail?: boolean | null
features?: Partial<AdminLayoutFeatures>
drawerConfig?: AdminLayoutDrawerConfig
}
const props = withDefaults(defineProps<Props>(), {
systemTitle: '管理系統',
systemSubtitle: 'Campus System',
themeToggleLabel: '切換主題',
logoutLabel: '登出',
sidebarToggleLabel: '切換側欄',
favoriteHeaderLabel: '我的最愛',
favoriteItems: () => [],
menuItems: () => [
{ title: '首頁', path: '/' },
],
},
userProfile: {
type: Object,
default: () => ({
userProfile: () => ({
name: '王小明',
role: '資訊工程系 - 學生',
avatarText: '王',
}),
},
searchConfig: {
type: Object,
default: () => ({
searchConfig: () => ({
placeholder: '搜尋功能名稱... (試試「成績」、「選課」、「請假」)',
label: '搜尋',
}),
},
toolbarActions: {
type: Object,
default: () => ({
toolbarActions: () => ({
notificationsLabel: '通知',
messagesLabel: '訊息',
helpLabel: '說明',
settingsLabel: '設定',
}),
},
toolbarCounts: {
type: Object,
default: () => ({
toolbarCounts: () => ({
notifications: 0,
messages: 0,
}),
},
favoritesConfig: {
type: Object,
default: () => ({
favoritesConfig: () => ({
label: '常用',
addLabel: '新增常用',
showAdd: false,
}),
},
breadcrumbConfig: {
type: Object,
default: () => ({
breadcrumbConfig: () => ({
homeLabel: '首頁',
homeDisabled: true,
homeIcon: mdiHome,
}),
},
breadcrumbItems: {
type: Array,
default: () => [],
},
favoritesBarVisible: {
type: [Boolean, null],
default: null,
},
breadcrumbBarVisible: {
type: [Boolean, null],
default: null,
},
isRail: {
type: [Boolean, null],
default: null,
},
features: {
type: Object,
default: () => ({
showThemeToggle: false,
showFavorites: true,
showBreadcrumb: true,
showSearch: true,
showToolbarActions: true,
showUserInfo: true,
showMenuHeader: false,
}),
},
drawerConfig: {
type: Object,
default: () => ({
breadcrumbItems: () => [],
favoritesBarVisible: null,
breadcrumbBarVisible: null,
isRail: null,
features: () => ({
showThemeToggle: false,
showFavorites: true,
showBreadcrumb: true,
showSearch: true,
showToolbarActions: true,
showUserInfo: true,
}),
drawerConfig: () => ({
width: 280,
railWidth: 56,
}),
},
})
// Feature toggle: UI 區塊顯示
@@ -296,39 +290,16 @@ const branding = computed(() => ({
subtitle: props.systemSubtitle,
}))
// feature toggles & layout
const display = useDisplay()
const isMobile = computed(() => display.mdAndDown.value)
const drawer = ref(true)
const mobileFavoritesPanel = ref(false)
const localIsRail = ref(false)
const isRail = computed({
get: () => (props.isRail ?? localIsRail.value),
set: (value) => {
if (props.isRail === null) {
localIsRail.value = value
return
}
emit('update:isRail', value)
},
})
const opened = ref([])
const appBarRef = ref(null)
const appBarHeight = ref(0)
const appBarRef = ref<HTMLElement | null>(null)
const helpWidgetVisible = ref(false)
const drawerWidth = computed(() => props.drawerConfig?.width)
const railWidth = computed(() => props.drawerConfig?.railWidth)
// i18n computed text
const searchValue = ref('')
const { toggleTheme: switchTheme } = useThemeToggle()
// links/settings refs
const theme = useTheme()
const availableThemeNames = computed(() =>
Object.keys(theme.themes.value ?? {}).filter((name) => name.startsWith('theme'))
)
// composed computed objects
const breadcrumbConfig = computed(() => ({ ...defaultBreadcrumbConfig, ...props.breadcrumbConfig }))
const breadcrumbItems = computed(() => {
@@ -342,31 +313,46 @@ const breadcrumbItems = computed(() => {
]
})
// event handlers / API
const {
drawer,
goToMobileLevel,
handleMobileMenuClick,
handleSelectFavorite,
handleUnshrink,
isRail,
mainStyle,
mobileCurrentItems,
mobileCurrentLevel,
mobileFavoritesPanel,
mobileMenuLevels,
openMobileFavoritesPanel,
opened,
showBreadcrumbBar,
showFavoritesBar,
toggleFavoritesBar,
toggleSidebar,
} = useAdminLayoutState({
appBarRef,
breadcrumbBarVisible: props.breadcrumbBarVisible,
emitUpdateBreadcrumbBarVisible: (value) => emit('update:breadcrumbBarVisible', value),
emitUpdateFavoritesBarVisible: (value) => emit('update:favoritesBarVisible', value),
emitUpdateIsRail: (value) => emit('update:isRail', value),
favoritesBarVisible: props.favoritesBarVisible,
isMobile,
isRail: props.isRail,
menuItems: props.menuItems,
onToggleSidebar: (payload) => emit('toggle-sidebar', payload),
})
function toggleTheme () {
const names = availableThemeNames.value
if (names.length === 0) return
const current = theme.global.name.value
const next = getNextThemeName(names, current)
const next = switchTheme()
if (!next) return
theme.change(next)
emit('toggle-theme', next)
}
function toggleSidebar () {
if (isMobile.value) {
drawer.value = !drawer.value
} else {
isRail.value = !isRail.value
}
emit('toggle-sidebar', { drawer: drawer.value, rail: isRail.value })
}
const emitLogout = () => emit('logout')
// 將工具列按鈕行為轉成統一的 action 事件,交由外層應用處理
function handleAction (type) {
function handleAction (type: AdminLayoutActionType) {
if (type === 'help') {
helpWidgetVisible.value = true
}
@@ -385,143 +371,30 @@ function emitAddFavorite () {
emit('add-favorite')
}
function emitRemoveFavorite (item) {
function emitRemoveFavorite (item: AdminLayoutMenuItem) {
emit('remove-favorite', item)
}
function handleSelectFavorite (item) {
handleSelect(item)
mobileFavoritesPanel.value = false
function onSelectFavorite (item: AdminLayoutMenuItem) {
handleSelectFavorite(item, handleSelect)
}
const mobileMenuPath = ref([])
const mobileCurrentItems = computed(() =>
mobileMenuPath.value.reduce((items, currentItem) => currentItem?.subItems ?? [], props.menuItems || [])
)
const mobileCurrentLevel = computed(() => mobileMenuPath.value.length + 1)
const mobileMenuLevels = computed(() =>
Array.from({ length: mobileCurrentLevel.value }, (_, index) => ({
level: index + 1,
title: index === 0 ? '主選單' : (mobileMenuPath.value[index - 1]?.title ?? `${index + 1}`),
}))
)
function goToMobileLevel (level) {
mobileFavoritesPanel.value = false
mobileMenuPath.value = mobileMenuPath.value.slice(0, Math.max(0, level - 1))
function onMobileMenuClick (item: AdminLayoutMenuItem) {
handleMobileMenuClick(item, handleSelect)
}
function openMobileFavoritesPanel () {
mobileMenuPath.value = []
mobileFavoritesPanel.value = true
}
function handleMobileMenuClick (item) {
if (item?.subItems?.length) {
mobileMenuPath.value = [...mobileMenuPath.value, item]
return
}
handleSelect(item)
}
const localFavoritesBarVisible = ref(true)
const localBreadcrumbBarVisible = ref(true)
const showFavoritesBar = computed({
get: () => (props.favoritesBarVisible ?? localFavoritesBarVisible.value),
set: (value) => {
if (props.favoritesBarVisible === null) {
localFavoritesBarVisible.value = value
return
}
emit('update:favoritesBarVisible', value)
},
})
const showBreadcrumbBar = computed({
get: () => (props.breadcrumbBarVisible ?? localBreadcrumbBarVisible.value),
set: (value) => {
if (props.breadcrumbBarVisible === null) {
localBreadcrumbBarVisible.value = value
return
}
emit('update:breadcrumbBarVisible', value)
},
})
function toggleFavoritesBar (nextValue) {
showFavoritesBar.value = typeof nextValue === 'boolean' ? nextValue : !showFavoritesBar.value
}
let appBarObserver
// 量測 v-app-bar 實際高度(常用功能/麵包屑顯示時會改變)
function updateAppBarHeight () {
const el = appBarRef.value?.$el ?? appBarRef.value
if (!el) return
appBarHeight.value = Math.round(el.getBoundingClientRect().height || 0)
}
onMounted(() => {
// 初次量測高度
updateAppBarHeight()
if (typeof ResizeObserver === 'undefined') return
const el = appBarRef.value?.$el ?? appBarRef.value
if (!el) return
// 監聽高度變化,讓 v-main paddingTop 同步更新
appBarObserver = new ResizeObserver(() => updateAppBarHeight())
appBarObserver.observe(el)
})
onBeforeUnmount(() => {
if (!appBarObserver) return
appBarObserver.disconnect()
appBarObserver = null
})
function handleUnshrink () {
isRail.value = false
}
function handleSelect (item) {
function handleSelect (item: AdminLayoutMenuItem) {
emit('select', item)
if (isMobile.value) drawer.value = false
}
const mainStyle = computed(() => {
const appBarHeightValue = appBarHeight.value
return {
// 以 paddingTop 騰出 appBar 空間,避免內容被遮擋
paddingTop: appBarHeightValue ? `${appBarHeightValue}px` : undefined,
// 固定 v-main 高度,讓內容區塊能在固定高度內滾動
height: '100vh',
minHeight: 0,
flex: '1 1 0',
}
})
// watch(isMobile, (value) => {
// if (value) {
// isRail.value = false
// }
// })
watch(isMobile, (value) => {
if (!value) {
mobileFavoritesPanel.value = false
mobileMenuPath.value = []
}
})
watch(drawer, (value) => {
if (!value) {
mobileFavoritesPanel.value = false
mobileMenuPath.value = []
}
})
function getMobileMenuBtnVariant (level) {
function getMobileMenuBtnVariant (level: number) {
return !mobileFavoritesPanel.value && level === mobileCurrentLevel.value
? 'flat'
: 'outlined'
}
function getMobileMenuBtnColor (level) {
function getMobileMenuBtnColor (level: number) {
return level === mobileCurrentLevel.value ? 'primary' : 'secondary'
}
</script>