Refactor MasterDetailMntC.vue for improved readability and consistency

This commit is contained in:
skytek_xinliang
2026-03-30 09:18:55 +08:00
parent 7591ecd062
commit 16b58fbf7a
66 changed files with 2071 additions and 777 deletions
+117 -41
View File
@@ -1,14 +1,25 @@
<template>
<!-- 根據路由設定 meta.layout 動態切換佈局 -->
<component
:is="activeLayout" v-bind="layoutProps" v-model:breadcrumb-bar-visible="favoritesStore.breadcrumbBarVisible"
:is="activeLayout"
v-bind="layoutProps"
v-model:breadcrumb-bar-visible="favoritesStore.breadcrumbBarVisible"
v-model:favorites-bar-visible="favoritesStore.favoritesBarVisible"
v-model:is-rail="menuStore.isRail" @action="handleLayoutAction" @logout="handleLogout"
@remove-favorite="handleRemoveFavorite" @search="handleSearch" @select="handleSelect">
v-model:is-rail="menuStore.isRail"
@action="handleLayoutAction"
@logout="handleLogout"
@remove-favorite="handleRemoveFavorite"
@search="handleSearch"
@select="handleSelect"
>
<template #breadcrumb-actions>
<v-btn
color="secondary" :disabled="isFavoriteActionDisabled" size="small" variant="outlined"
@click="toggleFavorite">
color="secondary"
:disabled="isFavoriteActionDisabled"
size="small"
variant="outlined"
@click="toggleFavorite"
>
<v-icon class="mr-1" size="14" :icon="favoriteActionIcon" />
{{ favoriteActionLabel }}
</v-btn>
@@ -21,19 +32,31 @@ color="secondary" :disabled="isFavoriteActionDisabled" size="small" variant="out
<template v-if="showTabs">
<div class="d-flex flex-column h-100">
<v-tabs
v-model="activeTab" bg-color="background" color="primary" density="compact" show-arrows
style="flex-shrink: 0;">
v-model="activeTab"
bg-color="background"
color="primary"
density="compact"
show-arrows
style="flex-shrink: 0"
>
<v-tab v-for="tab in tabs" :key="tab.path" border="sm" :to="tab.path" :value="tab.path">
{{ tab.title }}
<v-btn
class="pl-2" color="grey" density="compact" :disabled="tabs.length <= 1" icon size="x-small"
variant="text" @click.prevent.stop="closeTab(tab.path)">
class="pl-2"
color="grey"
density="compact"
:disabled="tabs.length <= 1"
icon
size="x-small"
variant="text"
@click.prevent.stop="closeTab(tab.path)"
>
<v-icon :icon="mdiClose" />
</v-btn>
</v-tab>
</v-tabs>
<div class="flex-grow-1 overflow-auto" style="min-height: 0;">
<div class="flex-grow-1 overflow-auto" style="min-height: 0">
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" :key="route.fullPath" />
@@ -50,13 +73,26 @@ class="pl-2" color="grey" density="compact" :disabled="tabs.length <= 1" icon si
<v-dialog v-model="searchDialog" max-width="640">
<v-card>
<v-card-title class="text-h6 bg-primary-variant pa-4">搜尋結果</v-card-title>
<v-card-subtitle v-if="searchKeyword" class="pt-4">關鍵字{{ searchKeyword }}</v-card-subtitle>
<v-card-subtitle v-if="searchKeyword" class="pt-4"
>關鍵字{{ searchKeyword }}</v-card-subtitle
>
<v-card-text class="pt-2">
<v-alert v-if="searchResults.length === 0" class="mt-2" density="compact" type="info" variant="tonal">
<v-alert
v-if="searchResults.length === 0"
class="mt-2"
density="compact"
type="info"
variant="tonal"
>
查無結果
</v-alert>
<v-list v-else density="compact">
<v-list-item v-for="item in searchResults" :key="item.path" class="mb-2" @click="handleSearchSelect(item)">
<v-list-item
v-for="item in searchResults"
:key="item.path"
class="mb-2"
@click="handleSearchSelect(item)"
>
<template #prepend>
<v-icon v-if="item.icon" size="18" :icon="item.icon" />
</template>
@@ -82,7 +118,9 @@ class="pl-2" color="grey" density="compact" :disabled="tabs.length <= 1" icon si
<v-dialog v-model="messageStore.isOpen" max-width="720">
<v-card>
<v-card-title class="text-h6 bg-primary-variant pa-4">訊息清單</v-card-title>
<v-card-subtitle class="text-body-2 pt-4 text-medium-emphasis">僅示意資料不含延伸功能</v-card-subtitle>
<v-card-subtitle class="text-body-2 pt-4 text-medium-emphasis"
>僅示意資料不含延伸功能</v-card-subtitle
>
<v-card-text class="pa-4">
<!--
使用 v-data-iterator 進行資料展示
@@ -91,7 +129,12 @@ class="pl-2" color="grey" density="compact" :disabled="tabs.length <= 1" icon si
<v-data-iterator item-key="id" :items="messageItems" :items-per-page="-1">
<template #default="{ items }">
<v-list density="compact">
<v-list-item v-for="wrapped in items" :key="resolveMessageItem(wrapped).id" border="sm" class="pa-2 mb-1">
<v-list-item
v-for="wrapped in items"
:key="resolveMessageItem(wrapped).id"
border="sm"
class="pa-2 mb-1"
>
<template #prepend>
<v-avatar color="primary" size="28" variant="tonal">
<v-icon size="16" :icon="resolveMessageItem(wrapped).icon" />
@@ -115,14 +158,36 @@ class="pl-2" color="grey" density="compact" :disabled="tabs.length <= 1" icon si
</v-dialog>
<v-snackbar
v-model="snackbar.visible" :color="snackbar.color" :location="snackbar.location"
:timeout="snackbar.timeout" :variant="snackbar.variant">
v-model="snackbar.visible"
:color="snackbar.color"
:location="snackbar.location"
:timeout="snackbar.timeout"
:variant="snackbar.variant"
>
{{ snackbar.message }}
</v-snackbar>
</template>
<script setup>
import { mdiAccountGroup, mdiBellOutline, mdiCalendarOutline, mdiChartBoxOutline, mdiClose, mdiCloseCircle, mdiCog, mdiDomain, mdiFileDocumentOutline, mdiFileTreeOutline, mdiHome, mdiHomeCityOutline, mdiMenu, mdiPlusCircle, mdiSchoolOutline, mdiTableEdit, mdiViewDashboardVariant } from '@mdi/js'
import {
mdiAccountGroup,
mdiBellOutline,
mdiCalendarOutline,
mdiChartBoxOutline,
mdiClose,
mdiCloseCircle,
mdiCog,
mdiDomain,
mdiFileDocumentOutline,
mdiFileTreeOutline,
mdiHome,
mdiHomeCityOutline,
mdiMenu,
mdiPlusCircle,
mdiSchoolOutline,
mdiTableEdit,
mdiViewDashboardVariant,
} from '@mdi/js'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import SKAdminLayout from '@/components/layouts/SKAdminLayout.vue'
@@ -194,7 +259,7 @@ const activeLayout = computed(() => {
return layoutMap[route.meta.layout] || SKAdminLayout
})
function buildMergedMenuItems (items) {
function buildMergedMenuItems(items) {
const flatPaths = new Set()
const collectPaths = (list) => {
for (const item of list || []) {
@@ -249,7 +314,7 @@ const layoutProps = computed(() => {
return {}
})
function handleSelect (item) {
function handleSelect(item) {
console.log('Selected:', item)
if (item.path) {
router.push(item.path)
@@ -260,7 +325,7 @@ const searchDialog = ref(false)
const searchKeyword = ref('')
const searchResults = ref([])
function buildSearchResults (items, keyword, parents = []) {
function buildSearchResults(items, keyword, parents = []) {
const results = []
for (const item of items || []) {
const currentParents = item?.title ? [...parents, item.title] : parents
@@ -283,7 +348,7 @@ function buildSearchResults (items, keyword, parents = []) {
}
// 接收 Layout 的搜尋事件:僅在「按鈕或 Enter」時觸發
function handleSearch (value) {
function handleSearch(value) {
const keyword = String(value ?? '').trim()
searchKeyword.value = keyword
if (!keyword) {
@@ -300,7 +365,7 @@ function handleSearch (value) {
}
// 點擊搜尋結果後導頁(行為等同選單點擊)
function handleSearchSelect (item) {
function handleSearchSelect(item) {
searchDialog.value = false
handleSelect(item)
}
@@ -314,7 +379,7 @@ const messageItems = [
]
// v-data-iterator 會包裝 items,這裡取回原始資料物件
function resolveMessageItem (wrapped) {
function resolveMessageItem(wrapped) {
if (wrapped && typeof wrapped === 'object' && 'raw' in wrapped) {
return wrapped.raw
}
@@ -323,14 +388,14 @@ function resolveMessageItem (wrapped) {
// 由 layout 的 action 事件統一進入此處處理
// 目前只處理訊息中心,其他 action 可在此擴充
function handleLayoutAction (type) {
function handleLayoutAction(type) {
if (type === 'messages') {
messageStore.open()
return
}
}
function performLogout ({ message, color }) {
function performLogout({ message, color }) {
authStore.logout()
tabs.value = []
activeTab.value = null
@@ -338,16 +403,16 @@ function performLogout ({ message, color }) {
router.replace({ name: 'login' })
}
function handleLogout () {
function handleLogout() {
performLogout({ message: '登出成功', color: 'success' })
}
function handleForceLogout (event) {
function handleForceLogout(event) {
const message = event?.detail?.message || '請重新登入'
performLogout({ message, color: 'warning' })
}
function handleHttpToast (event) {
function handleHttpToast(event) {
const detail = event?.detail
const message = detail?.message
if (!message) return
@@ -377,7 +442,7 @@ const showTabs = computed(() => {
})
// 遞迴尋找標題
function findTitle (path) {
function findTitle(path) {
const recursiveFind = (items) => {
for (const item of items) {
if (item.path === path) return item.title
@@ -407,7 +472,7 @@ function findTitle (path) {
return path
}
function findMenuItem (path) {
function findMenuItem(path) {
const recursiveFind = (items) => {
for (const item of items) {
if (item.path === path) return item
@@ -426,7 +491,9 @@ const currentFavoriteInfo = computed(() => {
const path = route.path
const menuItem = findMenuItem(path)
const title =
menuItem?.title || (typeof route.meta?.title === 'string' ? route.meta.title : null) || findTitle(path)
menuItem?.title ||
(typeof route.meta?.title === 'string' ? route.meta.title : null) ||
findTitle(path)
return {
title,
path,
@@ -435,12 +502,16 @@ const currentFavoriteInfo = computed(() => {
})
const isCurrentFavorite = computed(() => favoritesStore.isFavorite(route.path))
const isFavoriteActionDisabled = computed(() => !currentFavoriteInfo.value?.path || route.path === '/')
const isFavoriteActionDisabled = computed(
() => !currentFavoriteInfo.value?.path || route.path === '/'
)
const favoriteActionLabel = computed(() => (isCurrentFavorite.value ? '移除常用' : '加入常用'))
const favoriteActionIcon = computed(() => (isCurrentFavorite.value ? mdiCloseCircle : mdiPlusCircle))
const favoriteActionIcon = computed(() =>
isCurrentFavorite.value ? mdiCloseCircle : mdiPlusCircle
)
function toggleFavoriteItem (item) {
function toggleFavoriteItem(item) {
if (!item?.path || item.path === '/') return
favoritesStore.toggle({
title: item.title || findTitle(item.path),
@@ -449,19 +520,19 @@ function toggleFavoriteItem (item) {
})
}
function toggleFavorite () {
function toggleFavorite() {
toggleFavoriteItem(currentFavoriteInfo.value)
}
function handleRemoveFavorite (item) {
function handleRemoveFavorite(item) {
toggleFavoriteItem(item)
}
function goHome () {
function goHome() {
router.push('/')
}
function updateBreadcrumbs () {
function updateBreadcrumbs() {
const resolvedTitle = findTitle(route.path)
const fallbackTitle =
resolvedTitle && resolvedTitle !== route.path
@@ -481,7 +552,12 @@ function updateBreadcrumbs () {
}
watch(
[() => route.path, () => menuStore.menuItems, () => menuStore.favoriteItems, () => favoritesStore.items],
[
() => route.path,
() => menuStore.menuItems,
() => menuStore.favoriteItems,
() => favoritesStore.items,
],
() => updateBreadcrumbs(),
{ immediate: true, deep: true }
)
@@ -502,7 +578,7 @@ watch(
{ immediate: true }
)
function closeTab (path) {
function closeTab(path) {
if (tabs.value.length <= 1) return
const index = tabs.value.findIndex((t) => t.path === path)