Photo101.vue 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. <script setup>
  2. import { ref, onMounted, defineEmits } from "vue";
  3. import { useMainStore } from "@/stores/store";
  4. // Axios
  5. import axios from "axios";
  6. // i18n
  7. import { useI18n } from "vue-i18n";
  8. // Qrcode.vue
  9. // import QrcodeVue from "qrcode.vue";
  10. const { t } = useI18n();
  11. const store = useMainStore();
  12. const vlogVideo = ref(null);
  13. const canvas = ref(null);
  14. const photo = ref(null);
  15. let showVideo = ref(true);
  16. const emit = defineEmits(["closeDialog"]);
  17. // 關閉父組件的 funFilterDialog
  18. function closeDialog() {
  19. emit("closeDialog");
  20. }
  21. onMounted(() => {
  22. // 請求使用者的攝影機
  23. navigator.mediaDevices
  24. .getUserMedia({ video: true })
  25. .then((stream) => {
  26. vlogVideo.value.srcObject = stream;
  27. })
  28. .catch((error) => {
  29. console.error("無法開啟攝影機", error);
  30. });
  31. });
  32. let loading = ref(false);
  33. let havePhoto = ref(false); // 拍照完成
  34. let isCompleted = ref(false); // 照片確認
  35. let countdown = ref(0);
  36. // 倒數五秒拍攝
  37. function startCountdown() {
  38. countdown.value = 5;
  39. loading.value = true;
  40. const interval = setInterval(() => {
  41. countdown.value -= 1;
  42. if (countdown.value === 0) {
  43. clearInterval(interval);
  44. takePhoto();
  45. }
  46. }, 1000);
  47. }
  48. startCountdown();
  49. let photoFile = ref(null);
  50. // 拍攝照片
  51. const takePhoto = () => {
  52. const context = canvas.value.getContext("2d");
  53. canvas.value.width = vlogVideo.value.videoWidth;
  54. canvas.value.height = vlogVideo.value.videoHeight;
  55. context.drawImage(
  56. vlogVideo.value,
  57. 0,
  58. 0,
  59. vlogVideo.value.videoWidth,
  60. vlogVideo.value.videoHeight
  61. );
  62. // 將畫布轉換成 base64 格式的 JPG 照片,不調整畫質
  63. photo.value = canvas.value.toDataURL("image/jpeg"); // 不設置壓縮品質
  64. if (photo.value) {
  65. showVideo.value = false;
  66. // 將 base64 轉換成 Blob
  67. const photoBlob = dataURLtoBlob(photo.value);
  68. // 將 Blob 轉換成 File
  69. photoFile.value = blobToFile(photoBlob, "photo.jpg"); // 設定檔案名稱為 JPG 格式
  70. havePhoto.value = true;
  71. loading.value = false;
  72. }
  73. };
  74. // 重新拍攝
  75. function reshoot() {
  76. // 清空照片和檔案資料
  77. photo.value = null;
  78. photoFile.value = null;
  79. // 顯示影片重新準備拍攝
  80. showVideo.value = true;
  81. havePhoto.value = false;
  82. loading.value = false;
  83. startCountdown();
  84. console.log("重新拍攝,重設狀態");
  85. }
  86. // base64 轉 Blob
  87. function dataURLtoBlob(dataURL) {
  88. const byteString = atob(dataURL.split(",")[1]);
  89. const mimeString = dataURL.split(",")[0].split(":")[1].split(";")[0];
  90. const ab = new ArrayBuffer(byteString.length);
  91. const ia = new Uint8Array(ab);
  92. for (let i = 0; i < byteString.length; i++) {
  93. ia[i] = byteString.charCodeAt(i);
  94. }
  95. return new Blob([ab], { type: mimeString });
  96. }
  97. // Blob 轉 File
  98. function blobToFile(blob, fileName) {
  99. return new File([blob], fileName, { type: blob.type });
  100. }
  101. let swapVideo = ref(null);
  102. let swapVideoSrc = ref(null);
  103. let form = ref(false);
  104. let email = ref(null);
  105. let formLoading = ref(false);
  106. let showAlert = ref(false);
  107. const required = (v) => {
  108. const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  109. return emailPattern.test(v) || "請輸入有效的 email 格式";
  110. };
  111. async function onSubmit() {
  112. formLoading.value = true;
  113. let url = `https://cmm.ai/postcard/fs/swap-face-video/${store.assignGender}?email_to=${email.value}`;
  114. const formData = new FormData();
  115. formData.append("file", photoFile.value);
  116. console.log("url", url);
  117. try {
  118. const response = await axios.post(url, formData);
  119. console.log("換臉影片 response", response);
  120. if (response.data === "OK") {
  121. showAlert.value = true;
  122. setTimeout(() => {
  123. showAlert.value = false;
  124. closeDialog();
  125. }, 3000);
  126. }
  127. formLoading.value = false;
  128. } catch (error) {
  129. console.log("error", error);
  130. }
  131. }
  132. let isUploading = ref(false);
  133. let imageUrl = ref(null); // 圖片網址
  134. // 上傳
  135. async function upload() {
  136. isUploading.value = true;
  137. let url = `https://cmm.ai/postcard/fs/swap-face-capture/${store.assignGender}`;
  138. const formData = new FormData();
  139. formData.append("file", photoFile.value);
  140. console.log("url", url);
  141. try {
  142. const response = await axios.post(url, formData);
  143. console.log("換臉影片 response", response);
  144. if (response.status === 200) {
  145. isUploading.value = false;
  146. isCompleted.value = true;
  147. console.log("response.data", response.data);
  148. const filePath = response.data;
  149. const fileName = filePath.match(/results\/(.+)$/)[1];
  150. console.log("fileName", fileName);
  151. imageUrl.value = `https://cmm.ai/postcard/fs/result-image/${fileName}`;
  152. }
  153. } catch (error) {
  154. console.log("error", error);
  155. }
  156. }
  157. </script>
  158. <template>
  159. <div
  160. v-if="store.funFilterDialog"
  161. class="d-flex flex-column align-center justify-center"
  162. >
  163. <video v-show="showVideo" ref="vlogVideo" autoplay playsinline></video>
  164. <div
  165. v-if="photo && !isCompleted && !isUploading"
  166. class="w-100 d-flex flex-column align-center"
  167. >
  168. <img :src="photo" alt="captured image" class="captured-img" />
  169. <p class="prompt-text">
  170. {{ t("remove_mask_result") }}
  171. </p>
  172. <div class="d-flex mt-10">
  173. <v-btn variant="tonal" @click="reshoot()" class="me-5">
  174. {{ t("retake") }}
  175. </v-btn>
  176. <v-btn
  177. @click="upload()"
  178. color="primary"
  179. variant="elevated"
  180. :loading="isUploading"
  181. >
  182. {{ t("confirm") }}
  183. </v-btn>
  184. </div>
  185. </div>
  186. <video v-show="swapVideoSrc" ref="swapVideo" preload playsinline>
  187. <source :src="swapVideoSrc" type="video/mp4" />
  188. </video>
  189. <!-- 倒數計時動畫 -->
  190. <div v-if="countdown > 0" class="countdown">{{ countdown }}</div>
  191. <v-progress-circular
  192. v-else-if="loading"
  193. :size="50"
  194. color="primary"
  195. indeterminate
  196. ></v-progress-circular>
  197. <div
  198. class="d-flex flex-column align-center justify-center"
  199. v-if="isUploading"
  200. >
  201. <v-progress-circular
  202. :size="100"
  203. color="primary"
  204. indeterminate
  205. class="my-10"
  206. ></v-progress-circular>
  207. <p class="mb-10 text-center">{{ t("postcard_being_created") }}</p>
  208. </div>
  209. <div
  210. v-if="isCompleted && imageUrl"
  211. class="my-10 d-flex flex-column align-center justify-center"
  212. >
  213. <img :src="imageUrl" alt="" class="w-100" style="object-fit: contain" />
  214. <p class="mt-5 font-weight-bold">
  215. {{ t("qrcode_save_image") }}
  216. </p>
  217. </div>
  218. <canvas ref="canvas" style="display: none"></canvas>
  219. </div>
  220. </template>
  221. <style lang="scss" scoped>
  222. .captured-img {
  223. width: 65vw;
  224. max-height: 220px;
  225. margin: 1rem auto;
  226. object-fit: contain;
  227. }
  228. video {
  229. width: 100%;
  230. height: auto;
  231. margin: 1rem auto;
  232. object-fit: cover;
  233. }
  234. button {
  235. margin: auto;
  236. padding: 1.2rem 2rem;
  237. display: flex;
  238. justify-content: center;
  239. border-radius: 5px;
  240. letter-spacing: 3px;
  241. font-size: 1rem;
  242. }
  243. .countdown {
  244. position: absolute;
  245. top: 50%;
  246. left: 50%;
  247. transform: translate(-50%, -50%);
  248. font-size: 4em;
  249. font-weight: bold;
  250. color: #fff;
  251. }
  252. .result {
  253. width: 100%;
  254. display: flex;
  255. align-items: center;
  256. justify-content: center;
  257. img {
  258. width: 50%;
  259. }
  260. }
  261. .form-content {
  262. width: 100%;
  263. max-width: 500px;
  264. margin-bottom: 1.5rem;
  265. p {
  266. line-height: 2;
  267. letter-spacing: 1px;
  268. }
  269. }
  270. .alert-item {
  271. position: absolute;
  272. top: 50%;
  273. left: 50%;
  274. z-index: 100;
  275. transform: translate(-50%, -50%);
  276. .v-alert {
  277. width: 500px;
  278. padding: 1rem;
  279. .v-alert-title {
  280. margin-bottom: 0.5rem;
  281. }
  282. }
  283. }
  284. .prompt-text {
  285. color: #939393;
  286. }
  287. </style>