-import type { FunctionalComponent } from 'vue'
+import type { Component, FunctionalComponent } from 'vue'
import { useRoute } from 'vue-router'
import LayoutAside from './layout-aside.vue'
+import LayoutMain from './layout-main'
defineOptions({ name: 'MkViewLayout', inheritAttrs: false })
-const props = defineProps<{
- title?: string
-}>()
+const props = withDefaults(
+ defineProps<{
+ loading?: boolean
+ title?: string
+ }>(),
+ {
+ loading: false,
+ },
+)
defineSlots<{
aside?: (props: { Header: FunctionalComponent; title: string }) => unknown
- default?: () => unknown
- header?: (props: { title: string }) => unknown
+ default?: (props: { Header: Component; title: string }) => unknown
top?: () => unknown
}>()
const route = useRoute()
const title = computed(() => props.title || route.meta.title || '')
+const slots = useSlots()
+const fallbackTitle = computed(() =>
+ !slots.default || slots.default.length === 0 ? title.value : '',
+)
-
+
@@ -33,17 +43,11 @@ const title = computed(() => props.title || route.meta.title || '')
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/ui/src/components/global/mk-view-layout/layout-main.ts b/ui/src/components/global/mk-view-layout/layout-main.ts
new file mode 100644
index 00000000000..1a611fe193d
--- /dev/null
+++ b/ui/src/components/global/mk-view-layout/layout-main.ts
@@ -0,0 +1,95 @@
+import {
+ Fragment,
+ createTextVNode,
+ defineComponent,
+ h,
+ isVNode,
+ type Component,
+ type Slots,
+ type SlotsType,
+ type VNode,
+} from 'vue'
+import { ElScrollbar } from 'element-plus'
+
+type LayoutMainSlotProps = {
+ Header: Component
+ title: string
+}
+
+const LayoutMainHeader = defineComponent({
+ name: 'MkViewLayoutMainHeader',
+ setup(_, { slots }) {
+ return () => slots.default?.()
+ },
+})
+
+function flattenSlotNodes(children: unknown): VNode[] {
+ const childNodes = Array.isArray(children) ? children : [children]
+
+ return childNodes.flatMap((child) => {
+ if (child === undefined || child === null || typeof child === 'boolean') return []
+ if (!isVNode(child)) return [createTextVNode(String(child))]
+ if (child.type === Fragment) return flattenSlotNodes(child.children)
+ return [child]
+ })
+}
+
+function getHeaderNodes(headerNode: VNode) {
+ const headerSlots = headerNode.children as Slots | null
+ return flattenSlotNodes(headerSlots?.default?.())
+}
+
+export default defineComponent({
+ name: 'MkViewLayoutMain',
+ props: {
+ fallbackTitle: {
+ default: '',
+ type: String,
+ },
+ title: {
+ default: '',
+ type: String,
+ },
+ },
+ slots: Object as SlotsType<{
+ default?: (props: LayoutMainSlotProps) => VNode[]
+ }>,
+ setup(props, { slots }) {
+ return () => {
+ const headerNodes: VNode[] = []
+ const contentNodes: VNode[] = []
+
+ flattenSlotNodes(slots.default?.({ Header: LayoutMainHeader, title: props.title })).forEach(
+ (node) => {
+ if (node.type === LayoutMainHeader) {
+ headerNodes.push(...getHeaderNodes(node))
+ return
+ }
+
+ contentNodes.push(node)
+ },
+ )
+
+ const resolvedHeaderNodes =
+ headerNodes.length > 0
+ ? headerNodes
+ : props.fallbackTitle
+ ? [h('h4', props.fallbackTitle)]
+ : []
+
+ return h('main', { class: 'flex min-w-0 flex-1 flex-col px-6 mb-6' }, [
+ resolvedHeaderNodes.length
+ ? h('header', { class: 'flex-between shrink-0 py-4' }, resolvedHeaderNodes)
+ : null,
+ h(
+ ElScrollbar,
+ {
+ class: 'min-h-0 flex-1',
+ viewClass: 'flex min-h-full flex-col',
+ },
+ { default: () => contentNodes },
+ ),
+ ])
+ }
+ },
+})
diff --git a/ui/src/components/mk-search-list/index.vue b/ui/src/components/mk-search-list/index.vue
index c3c3bbdd3b7..a18b0e7ab73 100644
--- a/ui/src/components/mk-search-list/index.vue
+++ b/ui/src/components/mk-search-list/index.vue
@@ -140,7 +140,9 @@ function selectRow(row: T, index: number) {