camera.page.vue 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. <script setup lang="ts">
  2. import { useTitle } from '@/hooks/useTitle';
  3. import { Dialog, Toast } from '@/platform';
  4. import { saveFileMethod, uploadFileMethod } from '@/request/api/camera.api';
  5. import { useVisitor } from '@/stores';
  6. import { tryOnMounted, tryOnUnmounted } from '@vueuse/core';
  7. import { useForm, useRequest } from 'alova/client';
  8. import Segmented, { type ConfigProps } from './camera.config';
  9. import Camera from './camera.vue';
  10. let audio: HTMLAudioElement | void;
  11. const router = useRouter();
  12. const title = useTitle();
  13. const visitor = useVisitor();
  14. const { form: dataset, loading: submitting, send: submit } = useForm(data => saveFileMethod(data), {
  15. initialForm: { patientId: visitor.patientId } as Record<string, any>,
  16. }).onSuccess(({ data }) => {
  17. visitor.resultId = data.resultId;
  18. router.replace(data.route);
  19. }).onError(() => {
  20. handle();
  21. step.value = 1;
  22. });
  23. const step = ref(0);
  24. const snapshot = ref<string | void>();
  25. const config = shallowRef<ConfigProps>();
  26. const showExample = ref(false);
  27. watch([ step, snapshot ], ([ step, snapshot ], old, onCleanup) => {
  28. const { before, after, ..._config } = Segmented[ step - 1 ];
  29. const old_audio = config.value?.audio;
  30. config.value = Object.assign(_config, snapshot ? after : before);
  31. if ( old_audio !== config.value.audio ) {
  32. audio?.pause();
  33. if ( config.value.audio && audio ) {
  34. audio.src = config.value.audio;
  35. audio.play()
  36. .catch(() => Dialog.show({ message: '开始拍摄', theme: 'round-button' }))
  37. .then(() => audio!.play());
  38. }
  39. }
  40. title.value = config.value.title;
  41. });
  42. const cameraRef = useTemplateRef<InstanceType<typeof Camera>>('camera');
  43. const { loading: uploading, send: update, abort: stop } = useRequest((file: File) => uploadFileMethod(file), {
  44. immediate: false,
  45. });
  46. const handle = () => {
  47. if ( submitting.value ) return;
  48. if ( !showExample.value ) { snapshot.value = cameraRef.value?.handle(); }
  49. showExample.value = false;
  50. stop();
  51. };
  52. const next = async () => {
  53. if ( uploading.value || submitting.value ) return;
  54. if ( snapshot.value ) {
  55. uploading.value = true;
  56. const key = config.value!.key;
  57. const toast = Toast.loading(50, { message: '上传中...' });
  58. const response = await fetch(snapshot.value);
  59. const blob = await response.blob();
  60. const file = new File(
  61. [ blob ],
  62. `${ dataset.value.patientId }_${ key?.replace?.(/img/ig, '') }`,
  63. { type: blob.type },
  64. );
  65. try {
  66. const { url } = await update(file);
  67. dataset.value[ key ] = url;
  68. } finally {
  69. toast.close();
  70. }
  71. }
  72. if ( step.value === Segmented.length ) {
  73. submit();
  74. } else {
  75. handle();
  76. step.value += 1;
  77. }
  78. };
  79. tryOnMounted(() => {
  80. audio = document.createElement('audio');
  81. document.body.appendChild(audio);
  82. });
  83. tryOnUnmounted(() => {
  84. audio?.pause();
  85. audio = void 0;
  86. stop();
  87. });
  88. </script>
  89. <template>
  90. <div class="flex flex-col">
  91. <header class="flex flex-col justify-center px-24">
  92. <div class="text-3xl text-center" :class="{ required: config?.required }">{{ config?.label }}</div>
  93. <div class="mt-8 text-lg text-center tracking-wider leading-10">{{ config?.description }}</div>
  94. </header>
  95. <main class="flex justify-center items-center">
  96. <Camera ref="camera" v-bind="config?.video" @loaded="step = 1;">
  97. <template #shade="{scale}">
  98. <component :is="config?.shade" :scale="scale"></component>
  99. <img v-if="showExample && config?.example" :src="config.example" alt="示例" @click="showExample = false" />
  100. </template>
  101. </Camera>
  102. <div v-if="config?.example"
  103. class="size-40 absolute -top-8 right-2 cursor-pointer hover:text-primary"
  104. @click="showExample = !showExample"
  105. >
  106. <img class="size-full object-scale-down" :src="config?.example" alt="示例" />
  107. <div class="mt-2 text-xl text-center">示例</div>
  108. </div>
  109. </main>
  110. <footer class="flex flex-col justify-center items-center">
  111. <div v-if="snapshot" class="flex justify-evenly w-full cursor-pointer">
  112. <div class="">
  113. <img class="h-20" src="@/assets/images/button-cancel.png" alt="重拍" @click="handle()" /></div>
  114. <div class="cursor-pointer">
  115. <img class="h-20" src="@/assets/images/button-confirm.png" alt="确认" @click="next()" />
  116. </div>
  117. </div>
  118. <div v-else-if="step" class="h-min text-center cursor-pointer hover:text-primary" @click="handle()">
  119. <button class="size-28 border-8 rounded-full hover:border-primary"></button>
  120. <div class="mt-8 text-3xl">{{ showExample ? '开始拍照' : '点击拍照' }}</div>
  121. </div>
  122. </footer>
  123. </div>
  124. </template>
  125. <style scoped lang="scss">
  126. header, footer {
  127. flex: 1 1 20%;
  128. }
  129. main {
  130. position: relative;;
  131. flex: 1 1 40%;
  132. }
  133. .required {
  134. &::before {
  135. content: "*";
  136. margin-right: 4px;
  137. color: #f53030;
  138. }
  139. }
  140. </style>