Files
ReceiptGenerator/src/renderer/components/LoadingRegion.vue
T

47 lines
2.7 KiB
Vue

<script setup lang="ts">
import { nextTick,onBeforeUnmount,onMounted,ref,watch } from 'vue'
import { LoaderCircle,TriangleAlert,X } from '@lucide/vue'
const props=withDefaults(defineProps<{as?:string;loading?:boolean;label?:string;error?:string;progress?:number;delay?:number}>(),{as:'div',loading:false,label:'正在处理…',error:'',delay:0})
const emit=defineEmits<{dismissError:[]}>()
const container=ref<HTMLElement>(),visible=ref(false),center=ref(80)
let timer:ReturnType<typeof setTimeout>|undefined,observer:ResizeObserver|undefined,previousFocus:HTMLElement|null=null
function position(){
const rect=container.value?.getBoundingClientRect();if(!rect)return
const top=Math.max(0,rect.top),bottom=Math.min(innerHeight,rect.bottom)
center.value=bottom>top?(top+bottom)/2-rect.top:rect.height/2
}
watch(()=>props.loading,async loading=>{
clearTimeout(timer)
if(loading){
previousFocus=document.activeElement instanceof HTMLElement&&container.value?.contains(document.activeElement)?document.activeElement:null
if(props.delay)timer=setTimeout(()=>{visible.value=true;void nextTick(position)},props.delay)
else visible.value=true
}else{
visible.value=false
await nextTick()
if(previousFocus?.isConnected&&!previousFocus.matches(':disabled')&&document.activeElement===document.body)previousFocus.focus({preventScroll:true})
previousFocus=null
}
await nextTick();position()
},{immediate:true})
onMounted(()=>{
observer=new ResizeObserver(position);if(container.value)observer.observe(container.value)
window.addEventListener('scroll',position,{capture:true,passive:true});window.addEventListener('resize',position);position()
})
onBeforeUnmount(()=>{clearTimeout(timer);observer?.disconnect();window.removeEventListener('scroll',position,true);window.removeEventListener('resize',position)})
</script>
<template>
<component :is="as" ref="container" class="loading-region" :aria-busy="loading||undefined">
<div v-if="error" class="region-error" role="alert"><TriangleAlert :size="16"/><span>{{error}}</span><button class="icon-button" type="button" aria-label="关闭本板块错误提示" @click="emit('dismissError')"><X :size="15"/></button></div>
<div class="region-content" :inert="loading||undefined"><slot/></div>
<Transition name="region-loading">
<div v-if="visible" class="region-loading" role="status" aria-live="polite">
<div class="region-loading-indicator" :style="{top:center+'px'}"><LoaderCircle class="region-spinner" :size="30" :stroke-width="1.7"/><span>{{label}}</span>
<div v-if="progress!==undefined" class="region-loading-progress"><div><i :style="{width:Math.min(100,Math.max(0,progress))+'%'}"/></div><span>{{progress}}%</span></div>
<slot name="loading-actions"/>
</div>
</div>
</Transition>
</component>
</template>