| | |
| | | <script setup lang="ts"> |
| | | import { nextTick, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue'; |
| | | import Icon from '#/mobile/icon.vue'; |
| | | const props=defineProps<{disabled?:boolean;continuous?:boolean}>(); |
| | | import { speakDuplicateScan, type ScanVoiceOperation } from '#/mobile/scanVoice'; |
| | | const props=defineProps<{disabled?:boolean;continuous?:boolean;duplicateVoice?:boolean;voiceOperation?:ScanVoiceOperation}>(); |
| | | const emit=defineEmits<{code:[value:string]}>(); |
| | | const input=ref(''),error=ref(''),active=ref(false),starting=ref(false),video=ref<HTMLVideoElement>(),codeInput=ref<HTMLInputElement>(); |
| | | let stream:MediaStream|undefined,frame=0,generation=0,last=0; |
| | | async function testVoice(){error.value='';if(!await speakDuplicateScan(props.voiceOperation))error.value='语音播放失败,请检查媒体音量、浏览器声音权限和网络。';} |
| | | function stop(){generation++;active.value=false;starting.value=false;cancelAnimationFrame(frame);stream?.getTracks().forEach(t=>t.stop());stream=undefined;if(video.value)video.value.srcObject=null;} |
| | | function accept(value:string){if(props.disabled)return;stop();input.value=props.continuous?'':value;emit('code',value);} |
| | | watch([()=>props.disabled,()=>props.continuous],async ([disabled,continuous])=>{if(!disabled&&continuous){await nextTick();codeInput.value?.focus();}}); |
| | |
| | | <div class="m-panel"> |
| | | <button v-if="!active" type="button" class="m-secondary" :disabled="disabled||starting" @click="start"><Icon name="scan"/>{{starting?'正在请求相机权限…':'打开摄像头扫码'}}</button> |
| | | <template v-else><video ref="video" class="m-camera" muted playsinline aria-label="二维码取景"/><button type="button" class="m-secondary" @click="stop">关闭摄像头</button></template> |
| | | <button v-if="duplicateVoice" type="button" class="m-secondary" @click="testVoice">测试重复扫码语音</button> |
| | | <p v-if="duplicateVoice" class="m-muted">首次使用请点击测试并调高媒体音量。</p> |
| | | <p v-if="error" class="m-error" role="alert">{{error}}</p> |
| | | <form @submit.prevent="accept(input)"> |
| | | <label class="m-field" style="margin-top:16px"><span>码值 / 溯源链接</span><input ref="codeInput" v-model="input" autocomplete="off" :disabled="disabled" placeholder="手动输入或使用扫码设备" maxlength="2048" /></label> |