99 lines
5.2 KiB
Vue
99 lines
5.2 KiB
Vue
<script setup lang="ts">
|
|
import { computed, inject } from 'vue'
|
|
import type { KbxScreenDefinition, KbxScreenTemplateCode, KbxScreenType } from '@kbx/contracts'
|
|
import KbxCommandBar from './KbxCommandBar.vue'
|
|
import KbxDataState from './KbxDataState.vue'
|
|
import KbxPageHeader from './KbxPageHeader.vue'
|
|
import { KbxScreenUtilityHostKey, type KbxScreenUtilityTab } from '../utility/host'
|
|
import { KbxPermissionHostKey } from '../permission/host'
|
|
|
|
const props = withDefaults(defineProps<{
|
|
screen: KbxScreenDefinition
|
|
/** Template wrapper must state the canonical screen type it implements. */
|
|
expectedType?: KbxScreenType
|
|
templateCode?: KbxScreenTemplateCode
|
|
selectionCount?: number
|
|
can?: (permission: string) => boolean
|
|
breadcrumb?: string
|
|
status?: string
|
|
dirty?: boolean
|
|
stickyCommands?: boolean
|
|
commandBar?: boolean
|
|
suppressDefaultUtility?: boolean
|
|
}>(), {
|
|
expectedType: undefined,
|
|
templateCode: undefined,
|
|
selectionCount: 0,
|
|
breadcrumb: '',
|
|
status: '',
|
|
dirty: false,
|
|
stickyCommands: true,
|
|
commandBar: true,
|
|
suppressDefaultUtility: false,
|
|
})
|
|
|
|
const emit = defineEmits<{ command: [string] }>()
|
|
const utilityHost=inject(KbxScreenUtilityHostKey,null)
|
|
const permissionHost=inject(KbxPermissionHostKey,null)
|
|
const defaultUtilities=computed(()=>utilityHost?.available(props.screen)??[])
|
|
const utilityLabel:Record<KbxScreenUtilityTab,string>={help:'도움말',ai:'AI',suggestion:'제안'}
|
|
const templateMismatch=computed(()=>Boolean((props.expectedType&&props.screen.type!==props.expectedType)||(props.templateCode&&props.screen.templateCode!==props.templateCode)))
|
|
function canPermission(permission:string){return props.can?props.can(permission):(permissionHost?.has(permission)??true)}
|
|
const permissionDenied=computed(()=>Boolean(props.screen.permissions?.some(permission=>!canPermission(permission))))
|
|
</script>
|
|
|
|
<template>
|
|
<section
|
|
class="kbx-screen-frame"
|
|
:data-screen-id="screen.id"
|
|
:data-screen-type="screen.type"
|
|
:data-template="templateCode"
|
|
:data-status="status || undefined"
|
|
:data-dirty="dirty || undefined"
|
|
:data-access="permissionDenied?'denied':'allowed'"
|
|
>
|
|
<KbxPageHeader
|
|
data-kbx-surface="page-header"
|
|
:title="screen.title"
|
|
:description="screen.description"
|
|
:breadcrumb="breadcrumb"
|
|
:status="status"
|
|
:dirty="dirty"
|
|
>
|
|
<template #utility><slot name="utility" /><nav v-if="!suppressDefaultUtility && defaultUtilities.length" class="kbx-screen-frame__utility" aria-label="화면 보조기능"><button v-for="tab in defaultUtilities" :key="tab" type="button" @click="utilityHost?.open(screen,tab)">{{utilityLabel[tab]}}</button></nav></template>
|
|
</KbxPageHeader>
|
|
|
|
<KbxDataState v-if="templateMismatch" class="kbx-screen-frame__contract-error" state="error" title="화면 템플릿 구성이 올바르지 않습니다." detail="화면 정의와 표준 화면 유형이 일치하지 않습니다. 안전을 위해 업무 영역을 표시하지 않습니다." />
|
|
<KbxDataState v-else-if="permissionDenied" class="kbx-screen-frame__contract-error" state="error" title="이 화면을 사용할 권한이 없습니다." detail="현재 권한으로는 이 업무 화면을 열 수 없습니다. 메뉴 검색에서 사용 가능한 업무를 선택하세요." />
|
|
<template v-else>
|
|
<div
|
|
v-if="commandBar && ((screen.commands?.length ?? 0) > 0 || $slots.commands)"
|
|
class="kbx-screen-frame__commands"
|
|
data-kbx-surface="command-bar"
|
|
:class="{ 'is-sticky': stickyCommands }"
|
|
>
|
|
<slot name="commands">
|
|
<KbxCommandBar
|
|
:commands="screen.commands ?? []"
|
|
:selection-count="selectionCount"
|
|
:status="status"
|
|
:dirty="dirty"
|
|
:can="canPermission"
|
|
@command="emit('command', $event)"
|
|
/>
|
|
</slot>
|
|
</div>
|
|
|
|
<div v-if="$slots.notice" class="kbx-screen-frame__notice"><slot name="notice" /></div>
|
|
<div class="kbx-screen-frame__body"><slot /></div>
|
|
</template>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.kbx-screen-frame {display:flex;flex-direction:column;min-height:0;height:100%;background:var(--kbx-color-surface)}
|
|
.kbx-screen-frame__commands{min-height:var(--kbx-command-bar-height);padding:0 var(--kbx-screen-inline-padding);border-bottom:var(--kbx-border-width) solid var(--kbx-color-border);background:var(--kbx-color-surface);z-index:8}
|
|
.kbx-screen-frame__commands.is-sticky{position:sticky;top:0}.kbx-screen-frame__notice{padding:var(--kbx-space-2) var(--kbx-screen-inline-padding) 0}.kbx-screen-frame__body{min-height:0;flex:1;display:flex;flex-direction:column;gap:var(--kbx-screen-section-gap);padding:var(--kbx-space-2) var(--kbx-screen-inline-padding) var(--kbx-space-3)}
|
|
.kbx-screen-frame__utility{display:flex;align-items:center;gap:var(--kbx-space-1)}.kbx-screen-frame__utility button{height:var(--kbx-control-xs);padding:0 var(--kbx-space-2);border:var(--kbx-border-width) solid var(--kbx-color-border);border-radius:var(--kbx-radius-sm);background:var(--kbx-color-surface);color:var(--kbx-color-text);font-size:var(--kbx-font-xs)}.kbx-screen-frame__utility button:hover{background:var(--kbx-color-surface-hover);border-color:var(--kbx-color-border-strong)}.kbx-screen-frame__contract-error{margin:var(--kbx-space-3) var(--kbx-screen-inline-padding)}
|
|
</style>
|