Files
KArtSell.Aegis/docs/Design/kbx-foundation-v36/packages/kbx-ui/src/components/KbxLookup.vue
T

117 lines
7.1 KiB
Vue

<script setup lang="ts">
import { computed, inject, ref, watch } from 'vue'
import type { KbxFieldState, KbxLookupColumnDefinition, KbxLookupItem } from '@kbx/contracts'
import { kbxLookupRegistryKey } from '../lookup/registry'
import KbxLookupDialog from './KbxLookupDialog.vue'
const props = withDefaults(defineProps<{
modelValue: string | null
entity: string
label: string
required?: boolean
readonly?: boolean
disabled?: boolean
error?: string
warning?: string
helpText?: string
state?: KbxFieldState
columns?: KbxLookupColumnDefinition[]
pageSize?: number
}>(), { state:'default', pageSize:30 })
const emit = defineEmits<{
'update:modelValue': [string | null]
selected: [KbxLookupItem<string>]
}>()
const registry = inject(kbxLookupRegistryKey, {})
const provider = computed(() => registry[props.entity])
const code = ref('')
const displayName = ref('')
const open = ref(false)
const localError = ref('')
const resolving = ref(false)
let editingCode = false
let triggerElement: HTMLElement | null = null
let resolveSequence = 0
const uid=`kbx-lookup-${Math.random().toString(36).slice(2)}`
const messageId=`${uid}-message`
const effectiveState=computed(()=>props.error||localError.value?'error':props.warning?'warning':props.state)
const message=computed(()=>props.error||localError.value||props.warning||props.helpText||'')
watch(() => [props.modelValue, props.entity] as const, async ([id]) => {
const sequence=++resolveSequence
localError.value=''
if (!id) {
displayName.value = ''
if (!editingCode) code.value = ''
editingCode = false
resolving.value=false
return
}
if(!provider.value){code.value='';displayName.value='';localError.value='조회 공급자가 구성되지 않았습니다.';return}
resolving.value=true
try{
const item = await provider.value.resolveById(id)
if(sequence!==resolveSequence)return
if(item)applyDisplay(item)
else{code.value='';displayName.value='';localError.value='선택된 항목을 다시 확인하세요.'}
}catch{
if(sequence===resolveSequence)localError.value='선택 정보를 불러오지 못했습니다. 다시 시도하세요.'
}finally{if(sequence===resolveSequence)resolving.value=false}
}, { immediate: true })
function applyDisplay(item: KbxLookupItem<string>) { code.value=item.code; displayName.value=item.displayName }
async function resolveCode() {
const normalized=code.value.trim()
localError.value=''
if(!normalized){emit('update:modelValue',null);displayName.value='';return}
if (!provider.value){localError.value='조회 공급자가 구성되지 않았습니다.';return}
const sequence=++resolveSequence
resolving.value=true
try{
const item = await provider.value.resolveByCode(normalized)
if(sequence!==resolveSequence)return
if (item) select(item)
else localError.value = '일치하는 항목이 없습니다. F2로 조회하세요.'
}catch{
if(sequence===resolveSequence)localError.value='코드를 확인하지 못했습니다. 네트워크 상태를 확인하세요.'
}finally{if(sequence===resolveSequence)resolving.value=false}
}
function onCodeInput(value: string) {
code.value=value; displayName.value=''; localError.value=''; editingCode=true
emit('update:modelValue', null)
queueMicrotask(()=>{editingCode=false})
}
function show() {
if (props.readonly || props.disabled) return
if(!provider.value){localError.value='조회 공급자가 구성되지 않았습니다.';return}
triggerElement = document.activeElement instanceof HTMLElement ? document.activeElement : null
open.value = true
}
function select(item: KbxLookupItem<string>) {
++resolveSequence
applyDisplay(item); localError.value=''; resolving.value=false; emit('update:modelValue',item.id); emit('selected',item); open.value=false
}
function restoreFocus(){queueMicrotask(()=>triggerElement?.focus())}
function onRootKeydown(event:KeyboardEvent){if(event.key==='F2'){event.preventDefault();show()}}
</script>
<template>
<div class="kbx-lookup" :data-state="effectiveState" :aria-busy="resolving || undefined" @keydown="onRootKeydown">
<label class="kbx-lookup__label" :for="`${uid}-code`">{{ label }}<span v-if="required" aria-hidden="true"> *</span></label>
<div class="kbx-lookup__control">
<input :id="`${uid}-code`" :value="code" class="kbx-lookup__code" :readonly="readonly" :disabled="disabled" aria-label="코드" autocomplete="off" :aria-invalid="Boolean(error||localError)" :aria-describedby="message?messageId:undefined" @input="onCodeInput(($event.target as HTMLInputElement).value)" @keydown.enter.prevent="resolveCode">
<input :value="displayName" class="kbx-lookup__name" readonly :disabled="disabled" aria-label="선택된 이름">
<button type="button" class="kbx-lookup__search" :disabled="readonly || disabled || resolving" title="조회 F2" @click="show">{{ resolving ? '확인중' : '검색' }}</button>
</div>
<span v-if="message" :id="messageId" class="kbx-lookup__message" :role="error||localError?'alert':undefined">{{message}}</span>
<KbxLookupDialog v-model:visible="open" :entity="entity" :title="label" :initial-query="displayName || code" :columns="columns" :page-size="pageSize" @select="select" @update:visible="value => { open=value; if(!value)restoreFocus() }" />
</div>
</template>
<style scoped>
.kbx-lookup{display:grid;grid-template-columns:var(--kbx-label-width) minmax(0,1fr);gap:var(--kbx-space-2);align-items:start;font-size:var(--kbx-font-md)}.kbx-lookup__label{padding-top:var(--kbx-space-2);font-weight:500;white-space:nowrap}.kbx-lookup__control{display:grid;grid-template-columns:minmax(var(--kbx-lookup-code-width),auto) minmax(var(--kbx-lookup-name-min-width),1fr) var(--kbx-lookup-button-width);min-width:0}.kbx-lookup input,.kbx-lookup button{height:var(--kbx-control-height);border:var(--kbx-border-width) solid var(--kbx-color-border-strong);font:inherit;color:var(--kbx-color-text)}.kbx-lookup input{padding:0 var(--kbx-space-2);min-width:0;background:var(--kbx-color-surface)}.kbx-lookup__code{border-radius:var(--kbx-radius-sm) 0 0 var(--kbx-radius-sm)}.kbx-lookup__name{background:var(--kbx-color-surface-muted)!important;border-left:0!important}.kbx-lookup__search{border-radius:0 var(--kbx-radius-sm) var(--kbx-radius-sm) 0;background:var(--kbx-color-surface);border-left:0!important}.kbx-lookup[data-state="changed"] .kbx-lookup__code{border-color:var(--kbx-color-changed-border);background:var(--kbx-color-changed-surface)}.kbx-lookup[data-state="warning"] .kbx-lookup__code{border-color:var(--kbx-color-warning-border);background:var(--kbx-color-warning-surface)}.kbx-lookup[data-state="ai-suggested"] .kbx-lookup__code{border-color:var(--kbx-color-ai-border);background:var(--kbx-color-ai-surface)}.kbx-lookup[data-state="error"] .kbx-lookup__code{border-color:var(--kbx-color-danger);background:var(--kbx-color-danger-surface)}.kbx-lookup__message{grid-column:2;color:var(--kbx-color-text-muted);font-size:var(--kbx-font-xs)}.kbx-lookup[data-state="warning"] .kbx-lookup__message{color:var(--kbx-color-warning-text)}.kbx-lookup[data-state="error"] .kbx-lookup__message{color:var(--kbx-color-danger)}.kbx-lookup[data-state="ai-suggested"] .kbx-lookup__message{color:var(--kbx-color-ai-text)}
</style>