Refactor MasterDetailMntC.vue for improved readability and consistency
This commit is contained in:
+117
-41
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user