117 lines
7.1 KiB
Vue
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>
|