Initial commit: Receipt Studio desktop application
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user