SyuanYu 9 月之前
父節點
當前提交
0899a890aa

二進制
src/assets/img/race/亞洲_女.png


二進制
src/assets/img/race/亞洲_男.png


二進制
src/assets/img/race/歐美_女.png


二進制
src/assets/img/race/歐美_男.png


二進制
src/assets/img/race/穆斯林_女.png


二進制
src/assets/img/race/黑人_女.png


二進制
src/assets/img/race/黑人_男.png


+ 8 - 0
src/router/index.js

@@ -8,6 +8,7 @@ import Step_3 from "../views/Step_3.vue";
 import Step_4 from "../views/Step_4.vue";
 import Step_5 from "../views/Step_5.vue";
 import Step_6 from "../views/Step_6.vue";
+import Step_7 from "../views/Step_7.vue";
 // 天燈
 import Skylantern from '../views/Skylantern.vue'
 import SkylanternHome from '../views/SkylanternHome.vue'
@@ -60,6 +61,13 @@ const router = createRouter({
             requiresBgImg: true
           }
         },
+        {
+          path: 'step7',
+          component: Step_7,
+          meta: {
+            requiresBgImg: true
+          }
+        },
         // 天燈
         {
           path: '/skylantern',

+ 1 - 0
src/stores/store.js

@@ -5,6 +5,7 @@ export const useMainStore = defineStore('mainStore', {
         assignBgImg: "",
         styleNum: null,
         imgPath: "",
+        assignGender: ""
     }),
     getters: {
     },

+ 168 - 0
src/views/Step_2 backup.vue

@@ -0,0 +1,168 @@
+<script setup>
+import { useI18n } from "vue-i18n";
+import "animate.css";
+import Footer from "../components/Footer.vue";
+
+const { t } = useI18n();
+</script>
+
+<template>
+  <div class="content">
+    <ul class="step-list">
+      <li>
+        <img class="num" src="../assets/img/step-1.png" alt="" />
+        <p>{{ t("postcard.step2.text_1") }}</p>
+        <img class="arrow" src="../assets/img/arrow_b.png" alt="" />
+      </li>
+
+      <li>
+        <div class="img-box">
+          <img class="cover" src="../assets/img/step/1.png" alt="" />
+          <img class="arrow" src="../assets/img/arrow_b.png" alt="" />
+        </div>
+        <div class="step">
+          <img class="num" src="../assets/img/step-2.png" alt="" />
+          <p>{{ t("postcard.step2.text_2") }}</p>
+          <small>{{ t("postcard.step2.text_3") }}</small>
+        </div>
+      </li>
+
+      <li>
+        <div class="img-box">
+          <img class="cover" src="../assets/img/step/2.png" alt="" />
+          <img class="arrow" src="../assets/img/arrow_b.png" alt="" />
+        </div>
+        <div class="step">
+          <img class="num" src="../assets/img/step-3.png" alt="" />
+          <p>{{ t("postcard.step2.text_4") }}</p>
+          <!-- <small>人臉對準綠色框框</small> -->
+        </div>
+      </li>
+
+      <li>
+        <div class="img-box">
+          <img class="cover" src="../assets/img/step/3.png" alt="" />
+        </div>
+        <div class="step">
+          <img class="num" src="../assets/img/step-4.png" alt="" />
+          <p>{{ t("postcard.step2.text_5") }}</p>
+          <small>{{ t("postcard.step2.text_6") }}</small>
+        </div>
+      </li>
+    </ul>
+
+    <router-link to="/step3" class="main-btn">
+      {{ t("next_step") }}
+    </router-link>
+
+    <Footer url="/step1" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+p {
+  font-size: 1.125rem;
+
+  @media (max-width: 600px) {
+    font-size: 1rem;
+  }
+}
+
+span {
+  font-size: 1rem;
+
+  @media (max-width: 600px) {
+    font-size: 0.75rem;
+  }
+}
+
+.step-list {
+  padding: 0;
+  margin-top: 4rem;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  list-style: none;
+
+  .num {
+    width: 2rem;
+    margin-bottom: 1rem;
+  }
+
+  .arrow {
+    width: 3rem;
+    margin-top: 1rem;
+  }
+
+  li {
+    max-width: 500px;
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 1.2rem;
+
+    &:first-child {
+      align-items: center;
+      flex-direction: column;
+    }
+
+    .step {
+      width: 15rem;
+      padding-top: 1.5rem;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+
+      @media (max-width: 600px) {
+        max-width: 10.5rem;
+        padding-top: 0;
+      }
+    }
+
+    small {
+      margin-top: 0.3rem;
+      color: white;
+      letter-spacing: 2px;
+    }
+
+    .img-box {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      margin-right: 1.5rem;
+
+      .cover {
+        max-width: 15rem;
+
+        @media (max-width: 600px) {
+          max-width: 9rem;
+        }
+      }
+    }
+  }
+}
+
+.content {
+  p {
+    &:first-child {
+      margin-bottom: 1.625rem;
+    }
+  }
+
+  .main-btn {
+    margin: 3rem auto 0;
+  }
+
+  .hashtag {
+    display: flex;
+    justify-content: center;
+
+    span {
+      color: white;
+      &:last-child {
+        margin-left: 20px;
+      }
+    }
+  }
+}
+</style>

+ 57 - 96
src/views/Step_2.vue

@@ -1,55 +1,48 @@
 <script setup>
+import { ref, reactive } from "vue";
+import { useMainStore } from "@/stores/store";
 import { useI18n } from "vue-i18n";
 import "animate.css";
 import Footer from "../components/Footer.vue";
 
 const { t } = useI18n();
+const store = useMainStore();
+
+// let assignGender = ref("");
+
+let genderList = reactive([
+  {
+    text: "男",
+    value: "male",
+    icon: "mdi-gender-male",
+  },
+  {
+    text: "女",
+    value: "female",
+    icon: "mdi-gender-female",
+  },
+]);
+
+function setGender(value) {
+  store.assignGender = value;
+  console.log("setGender", store.assignGender);
+}
 </script>
 
 <template>
   <div class="content">
-    <ul class="step-list">
-      <li>
-        <img class="num" src="../assets/img/step-1.png" alt="" />
-        <p>{{ t("postcard.step2.text_1") }}</p>
-        <img class="arrow" src="../assets/img/arrow_b.png" alt="" />
-      </li>
-
-      <li>
-        <div class="img-box">
-          <img class="cover" src="../assets/img/step/1.png" alt="" />
-          <img class="arrow" src="../assets/img/arrow_b.png" alt="" />
-        </div>
-        <div class="step">
-          <img class="num" src="../assets/img/step-2.png" alt="" />
-          <p>{{ t("postcard.step2.text_2") }}</p>
-          <small>{{ t("postcard.step2.text_3") }}</small>
-        </div>
-      </li>
-
-      <li>
-        <div class="img-box">
-          <img class="cover" src="../assets/img/step/2.png" alt="" />
-          <img class="arrow" src="../assets/img/arrow_b.png" alt="" />
-        </div>
-        <div class="step">
-          <img class="num" src="../assets/img/step-3.png" alt="" />
-          <p>{{ t("postcard.step2.text_4") }}</p>
-          <!-- <small>人臉對準綠色框框</small> -->
-        </div>
-      </li>
-
-      <li>
-        <div class="img-box">
-          <img class="cover" src="../assets/img/step/3.png" alt="" />
-        </div>
-        <div class="step">
-          <img class="num" src="../assets/img/step-4.png" alt="" />
-          <p>{{ t("postcard.step2.text_5") }}</p>
-          <small>{{ t("postcard.step2.text_6") }}</small>
-        </div>
-      </li>
-    </ul>
+    <div class="gender-btn">
+      <p class="mb-10">請選擇性別</p>
+
+      <button
+        v-for="item in genderList"
+        @click="setGender(item.value)"
+        :class="{ assign: store.assignGender === item.value }"
+      >
+        <v-icon :icon="item.icon" color="white"></v-icon>
+        <p>{{ item.text }}</p>
+      </button>
+    </div>
 
     <router-link to="/step3" class="main-btn">
       {{ t("next_step") }}
@@ -76,73 +69,41 @@ span {
   }
 }
 
-.step-list {
-  padding: 0;
-  margin-top: 4rem;
+.gender-btn {
   display: flex;
   flex-direction: column;
   align-items: center;
-  justify-content: center;
-  list-style: none;
-
-  .num {
-    width: 2rem;
-    margin-bottom: 1rem;
-  }
-
-  .arrow {
-    width: 3rem;
-    margin-top: 1rem;
-  }
+  margin-top: auto;
 
-  li {
-    max-width: 500px;
+  button {
     display: flex;
-    justify-content: space-between;
-    margin-bottom: 1.2rem;
-
-    &:first-child {
-      align-items: center;
-      flex-direction: column;
-    }
-
-    .step {
-      width: 15rem;
-      padding-top: 1.5rem;
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-
-      @media (max-width: 600px) {
-        max-width: 10.5rem;
-        padding-top: 0;
-      }
+    width: 14rem;
+    padding: 0.8rem;
+    margin-bottom: 2rem;
+    border-radius: 100px;
+    border: 3px solid var(--main-color);
+    background-color: var(--main-color);
+
+    .v-icon {
+      position: absolute;
     }
 
-    small {
-      margin-top: 0.3rem;
-      color: white;
-      letter-spacing: 2px;
+    p {
+      margin: auto;
     }
 
-    .img-box {
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      margin-right: 1.5rem;
-
-      .cover {
-        max-width: 15rem;
-
-        @media (max-width: 600px) {
-          max-width: 9rem;
-        }
-      }
+    &.assign {
+      border: 3px solid white;
+      background-color: #AE774F;
     }
   }
 }
 
 .content {
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+
   p {
     &:first-child {
       margin-bottom: 1.625rem;
@@ -150,7 +111,7 @@ span {
   }
 
   .main-btn {
-    margin: 3rem auto 0;
+    margin: auto auto 0;
   }
 
   .hashtag {

+ 4 - 237
src/views/Step_3.vue

@@ -1,5 +1,5 @@
 <script setup>
-import { ref, reactive, computed, onMounted } from "vue";
+import { ref, computed, onMounted } from "vue";
 import { useMainStore } from "@/stores/store";
 import { useRouter } from "vue-router";
 import { useI18n } from "vue-i18n";
@@ -7,9 +7,9 @@ import "animate.css";
 import axios from "axios";
 import Footer from "../components/Footer.vue";
 
+const { t } = useI18n();
 const router = useRouter();
 const store = useMainStore();
-const { t } = useI18n();
 
 const apiUrl = import.meta.env.VITE_API_URL;
 const imgUrl = import.meta.env.VITE_API_IMG_URL;
@@ -67,34 +67,6 @@ const currentPage = computed(
   () => Math.floor(currentIndex.value / perPage.value) + 1
 );
 
-// 測試欄位
-// let parameters = reactive({
-//   styel_name: "",
-//   prompt: "",
-//   negative_prompt: "",
-//   bg_img: "",
-//   styles: ["real"],
-// });
-
-// 算圖測試欄位
-// let runParameters = reactive({
-//   seed: "54987890",
-//   denoising_strength: "0.35",
-//   batch_size: "1",
-//   n_iter: "30",
-// });
-
-// let fileInput = ref(null);
-// let imgFile = ref(null);
-
-// function onFileChange() {
-//   console.log("fileInput", fileInput.value);
-//   if (fileInput.value.files.length) {
-//     imgFile.value = fileInput.value.files[0];
-//   }
-//   console.log("imgFile.value", imgFile.value);
-// }
-
 let parameterList = ref([]);
 
 // 背景清單
@@ -240,65 +212,6 @@ async function getParameters() {
   }
 }
 
-// async function setParameters() {
-//   let url = `${apiUrl}/sd/paprameter`;
-//   let getUrl = `${apiUrl}/sd/parameters`;
-
-//   if (assignBgImg.value === "") {
-//     alert("尚未選取背景圖");
-//     return;
-//   } else {
-//     parameters.bg_img = assignBgImg.value;
-//   }
-
-//   if (!imgFile.value) {
-//     alert("尚未上傳人物圖");
-//   }
-
-//   console.log("parameters", parameters);
-
-//   try {
-//     let response = await axios.post(url, parameters);
-//     console.log("setParameters", response);
-
-//     if (response.status === 200) {
-//       let getResponse = await axios.get(getUrl);
-//       console.log("getResponse", getResponse);
-
-//       // 算圖
-//       runImg(getResponse.data.length);
-//     }
-//   } catch (error) {
-//     console.log("error", error);
-//   }
-// }
-
-// let imgLoading = ref(false);
-// let imgPath = ref("");
-
-// async function runImg(styleNum) {
-//   imgPath.value = "";
-//   imgLoading.value = true;
-//   console.log("styleNum", styleNum);
-//   let url = `${apiUrl}/sd/run?seed=${runParameters.seed}&denoising_strength=${runParameters.denoising_strength}&batch_size=${runParameters.batch_size}&n_iter=${runParameters.n_iter}&style_num=${styleNum}`;
-
-//   // 人物圖
-//   const formData = new FormData();
-//   formData.append("file", imgFile.value);
-
-//   try {
-//     let response = await axios.post(url, formData);
-//     console.log("runImg", response);
-
-//     if (response.status === 200) {
-//       imgPath.value = response.data[0].path;
-//       imgLoading.value = false;
-//     }
-//   } catch (error) {
-//     console.log("error", error);
-//   }
-// }
-
 let alertShow = ref(false);
 
 function checkImg() {
@@ -350,14 +263,6 @@ function checkImg() {
 
         <p>{{ t(item.title) }}</p>
 
-        <!-- <img
-          class="cover"
-          :src="`http://172.104.93.163:3219/static/assets/img/bg/${item.bg_img}`"
-          alt=""
-        /> -->
-
-        <!-- <p>{{ item.bg_img.replace(".png", "") }}</p> -->
-
         <img
           v-if="item === assignBgImg"
           class="icon active"
@@ -369,88 +274,10 @@ function checkImg() {
 
       <span class="page-num">{{ currentPage }} / {{ totalPages }}</span>
 
-      <!-- <div class="upload-input">
-        <v-file-input
-          label="File input"
-          prepend-icon="mdi-camera"
-          variant="filled"
-        ></v-file-input>
-      </div> -->
-
-      <!-- 測試 -->
-      <!-- <div class="test-box">
-        <form>
-          <label for="styel_name">styel_name</label>
-          <input v-model="parameters.styel_name" id="styel_name" type="text" />
-
-          <label for="prompt">prompt</label>
-          <input v-model="parameters.prompt" id="prompt" type="text" />
-
-          <label for="negative_prompt">negative_prompt</label>
-          <input
-            v-model="parameters.negative_prompt"
-            id="negative_prompt"
-            type="text"
-          />
-
-          <label for="seed">seed</label>
-          <input
-            v-model="runParameters.seed"
-            id="seed"
-            type="text"
-          />
-
-          <label for="denoising_strength">denoising_strength</label>
-          <input
-            v-model="runParameters.denoising_strength"
-            id="denoising_strength"
-            type="text"
-          />
-
-          <label for="batch_size">batch_size</label>
-          <input
-            v-model="runParameters.batch_size"
-            id="batch_size"
-            type="text"
-          />
-
-          <label for="n_iter">n_iter</label>
-          <input
-            v-model="runParameters.n_iter"
-            id="n_iter"
-            type="text"
-          />
-
-          <label for="file">上傳人物照片</label>
-          <input
-            ref="fileInput"
-            v-on:change="onFileChange()"
-            type="file"
-            id="file"
-          />
-        </form>
-
-        <button @click="setParameters()" class="main-btn">
-          <img
-            v-if="imgLoading"
-            class="spinner"
-            src="../assets/img/Spinner-1s-200px.svg"
-            alt=""
-          />
-          <span v-else>測試算圖</span>
-        </button>
-
-        <div v-if="imgPath !== ''">
-          <a :href="imgPath" target="_blank">查看算圖結果</a>
-        </div>
-      </div> -->
-
       <a @click="checkImg()" href="javascript:;" class="main-btn">
         {{ t("next_step") }}
       </a>
 
-      <!-- <router-link to="/step5" class="main-btn">下一步</router-link> -->
-
       <div v-if="alertShow" class="alert-item">
         <v-alert border="top" type="warning" variant="outlined" class="mt-5">
           尚未選擇背景
@@ -463,66 +290,6 @@ function checkImg() {
 </template>
 
 <style lang="scss" scoped>
-// .test-box {
-//   form {
-//     display: flex;
-//     flex-direction: column;
-//     margin-bottom: 40px;
-//   }
-//   label {
-//     color: white;
-//     margin-bottom: 5px;
-//     letter-spacing: 1px;
-//   }
-
-//   input {
-//     padding: 10px;
-//     margin-bottom: 20px;
-//   }
-
-//   .spinner {
-//     width: 70px;
-//     margin-bottom: -10px;
-//   }
-
-//   a,
-//   #file {
-//     color: white;
-//   }
-
-//   a {
-//     text-align: center;
-//     display: block;
-//     margin-top: 10px;
-//   }
-
-//   .main-btn {
-//     margin: auto;
-//   }
-// }
-
-// .upload-input {
-//   width: 30rem;
-//   padding: 2rem;
-//   background: #fff;
-//   border-radius: 5px;
-// }
-
-// img {
-//   width: 100%;
-// }
-
-// .title {
-//   padding-top: 4rem;
-//   margin-bottom: 2rem;
-//   font-size: 1.625rem;
-
-//   @media (max-width: 600px) {
-//     padding-top: 2rem;
-//     font-size: 1.25rem;
-//   }
-// }
-
 .img-content {
   // height: 80vh;
   padding: 0 2rem;
@@ -542,8 +309,8 @@ function checkImg() {
 
     .cover {
       max-width: 100%;
-      width: 20rem;
-      height: 25vh;
+      width: 17rem;
+      height: 20vh;
       object-fit: cover;
     }
   }

+ 357 - 26
src/views/Step_4.vue

@@ -1,25 +1,255 @@
 <script setup>
+import { ref, computed, onMounted } from "vue";
 import { useMainStore } from "@/stores/store";
+import { useRouter } from "vue-router";
 import { useI18n } from "vue-i18n";
+import "animate.css";
+import axios from "axios";
 import Footer from "../components/Footer.vue";
 
 const { t } = useI18n();
+const router = useRouter();
 const store = useMainStore();
-console.log("step5 store.assignBgImg", store.assignBgImg);
+
+const apiUrl = import.meta.env.VITE_API_URL;
+const imgUrl = import.meta.env.VITE_API_IMG_URL;
+console.log("VITE_API_URL", apiUrl);
+
+let assignBgImg = ref("");
+
+function handleBgImg(item) {
+  console.log("name", item);
+  assignBgImg.value = item;
+  store.assignBgImg = item;
+  parameterList.value.filter((e, index) => {
+    if (e.bg_img === item.bg_img) {
+      store.styleNum = index;
+    }
+  });
+
+  console.log("store.assignBgImg", store.assignBgImg);
+  console.log("store.styleNum", store.styleNum);
+}
+
+onMounted(() => {
+  getParameters();
+});
+
+const currentPhotos = computed(() => {
+  const start = currentIndex.value;
+  const end = start + perPage.value;
+  return parameter.value.slice(start, end);
+});
+
+console.log("currentPhotos", currentPhotos);
+
+let currentIndex = ref(0);
+let perPage = ref(2);
+
+function prev() {
+  if (currentIndex.value > 0) {
+    currentIndex.value -= perPage.value;
+  }
+}
+
+function next() {
+  if (currentIndex.value + perPage.value < parameter.value.length) {
+    currentIndex.value += perPage.value;
+  }
+}
+
+// 計算頁數
+const totalPages = computed(() =>
+  Math.ceil(parameter.value.length / perPage.value)
+);
+
+const currentPage = computed(
+  () => Math.floor(currentIndex.value / perPage.value) + 1
+);
+
+let parameterList = ref([]);
+
+// 背景清單
+let parameter = ref([
+  {
+    bg_img: "臺北陽明山-母親節.png",
+    title: "taipei_yangmingshan",
+    description: "taipei_yangmingshan_description",
+  },
+  {
+    bg_img: "台南孔廟.png",
+    title: "tainan_confucius_temple",
+    description: "tainan_confucius_temple_description",
+  },
+  {
+    bg_img: "臺北中正紀念堂-2.png",
+    title: "taipei_chiang_kai_shek_memorial_hall_2",
+    description: "taipei_chiang_kai_shek_memorial_hall_2_description",
+  },
+  {
+    bg_img: "台東嘉明湖.png",
+    title: "taitung_jiaming_lake",
+    description: "taitung_jiaming_lake_description",
+  },
+  {
+    bg_img: "中秋節.png",
+    title: "mid_autumn_festival",
+    description: "mid_autumn_festival_description",
+  },
+  {
+    bg_img: "新北野柳女王頭.png",
+    title: "new_taipei_yehliu_queen_head",
+    description: "new_taipei_yehliu_queen_head_description",
+  },
+  {
+    bg_img: "基隆和平島公園.png",
+    title: "keelung_heping_island_park",
+    description: "keelung_heping_island_park_description",
+  },
+  {
+    bg_img: "台中日月潭.png",
+    title: "taichung_sun_moon_lake",
+    description: "taichung_sun_moon_lake_description",
+  },
+  {
+    bg_img: "臺北中正紀念堂.png",
+    title: "taipei_chiang_kai_shek_memorial_hall",
+    description: "taipei_chiang_kai_shek_memorial_hall_description",
+  },
+  {
+    bg_img: "台南鹽田.png",
+    title: "tainan_salt_field",
+    description: "tainan_salt_field_description",
+  },
+  {
+    bg_img: "高雄美麗島.png",
+    title: "kaohsiung_formosa_boulevard_station",
+    description: "kaohsiung_formosa_boulevard_station_description",
+  },
+  {
+    bg_img: "新北十分瀑布.png",
+    title: "new_taipei_shifen_waterfall",
+    description: "new_taipei_shifen_waterfall_description",
+  },
+  {
+    bg_img: "臺北故宮.png",
+    title: "taipei_national_palace_museum",
+    description: "taipei_national_palace_museum_description",
+  },
+  {
+    bg_img: "臺北故宮-2.png",
+    title: "taipei_national_palace_museum_2",
+    description: "taipei_national_palace_museum_2_description",
+  },
+  {
+    bg_img: "台中歌劇院.png",
+    title: "national_taichung_theater",
+    description: "national_taichung_theater_description",
+  },
+  {
+    bg_img: "嘉義森林之歌.png",
+    title: "chiayi_song_of_forest",
+    description: "chiayi_song_of_forest_description",
+  },
+  {
+    bg_img: "基隆八斗子鐵路.png",
+    title: "keelung_baduzi_railway",
+    description: "keelung_baduzi_railway_description",
+  },
+  {
+    bg_img: "花蓮清水斷崖.png",
+    title: "hualien_qingshui_cliff",
+    description: "hualien_qingshui_cliff_description",
+  },
+  {
+    bg_img: "澎湖.png",
+    title: "penghu",
+    description: "penghu_description",
+  },
+  {
+    bg_img: "南投清境農場.png",
+    title: "nantou_qingjing_farm",
+    description: "nantou_qingjing_farm_description",
+  },
+  {
+    bg_img: "高雄流行音樂中心.png",
+    title: "kaohsiung_music_center",
+    description: "kaohsiung_music_center_description",
+  },
+  {
+    bg_img: "花蓮金針花山.png",
+    title: "hualien_daylily_mountain",
+    description: "hualien_daylily_mountain_description",
+  },
+  {
+    bg_img: "新北九份老街.png",
+    title: "new_taipei_jiufen_old_street",
+    description: "new_taipei_jiufen_old_street_description",
+  },
+  {
+    bg_img: "嘉義阿里山小火車.png",
+    title: "chiayi_alishan_forest_railways",
+    description: "chiayi_alishan_forest_railways_description",
+  },
+  {
+    bg_img: "台中高美濕地.png",
+    title: "taichung_gaomei_wetland",
+    description: "taichung_gaomei_wetland_description",
+  },
+]);
+
+console.log("parameter", parameter.value);
+
+async function getParameters() {
+  let url = `${apiUrl}/sd/parameters`;
+
+  try {
+    let response = await axios.get(url);
+    parameterList.value = response.data;
+    console.log("parameterList", parameterList.value);
+  } catch (error) {
+    console.log("error", error);
+  }
+}
+
+let alertShow = ref(false);
+
+function checkImg() {
+  if (store.assignBgImg && store.assignBgImg !== "") {
+    alertShow.value = false;
+    router.push("/step5");
+  } else {
+    alertShow.value = true;
+    setTimeout(() => {
+      alertShow.value = false;
+    }, 2000);
+  }
+}
 </script>
 
 <template>
-  <div class="content main-bg">
-    <v-container
-      class="px-5 px-sm-15 mt-15 d-flex flex-column align-center justify-center"
-    >
-      <div>
+  <div class="content">
+    <p class="title">{{ t("postcard.step2.text_2") }}</p>
+    <div class="img-content">
+      <div class="slider-btn">
+        <button class="prev" @click="prev">
+          <img class="arrow" src="../assets/img/arrow_l.png" alt="" />
+        </button>
+        <button class="next" @click="next">
+          <img class="arrow" src="../assets/img/arrow_r.png" alt="" />
+        </button>
+      </div>
+
+      <div
+        @click="handleBgImg(item)"
+        v-for="item in currentPhotos"
+        class="bg-img"
+      >
         <v-img
-          max-width="500"
           cover
-          class="my-5 mx-auto"
-          :lazy-src="`http://172.104.93.163:3219/static/assets/img/bg/${store.assignBgImg.bg_img}`"
-          :src="`http://172.104.93.163:3219/static/assets/img/bg/${store.assignBgImg.bg_img}`"
+          class="cover"
+          :lazy-src="`http://172.104.93.163:3219/static/assets/img/bg/${item.bg_img}`"
+          :src="`http://172.104.93.163:3219/static/assets/img/bg/${item.bg_img}`"
         >
           <template v-slot:placeholder>
             <div class="d-flex align-center justify-center fill-height">
@@ -31,33 +261,134 @@ console.log("step5 store.assignBgImg", store.assignBgImg);
           </template>
         </v-img>
 
-        <p>{{ t(store.assignBgImg.title) }}</p>
+        <p>{{ t(item.title) }}</p>
 
-        <p
-          class="text-start px-5 my-10 description"
-          v-html="t(store.assignBgImg.description)"
-        ></p>
+        <img
+          v-if="item === assignBgImg"
+          class="icon active"
+          src="../assets/img/confirm.png"
+          alt=""
+        />
+        <img v-else class="icon" src="../assets/img/confirm-solid.png" alt="" />
       </div>
 
-      <router-link to="/step5" class="main-btn mt-auto">
-        {{ t("confirm") }}
-      </router-link>
-    </v-container>
+      <span class="page-num">{{ currentPage }} / {{ totalPages }}</span>
+
+      <a @click="checkImg()" href="javascript:;" class="main-btn">
+        {{ t("next_step") }}
+      </a>
+
+      <div v-if="alertShow" class="alert-item">
+        <v-alert border="top" type="warning" variant="outlined" class="mt-5">
+          尚未選擇背景
+        </v-alert>
+      </div>
+    </div>
 
     <Footer url="/step3" />
   </div>
 </template>
 
 <style lang="scss" scoped>
-.v-container {
-  min-height: 75vh;
-}
-
-.content {
-  min-height: 100vh;
+.img-content {
+  // height: 80vh;
+  padding: 0 2rem;
   display: flex;
   flex-direction: column;
   align-items: center;
-  justify-content: center;
+
+  @media (max-width: 600px) {
+    height: 100%;
+    padding: 0 4rem;
+  }
+
+  .bg-img {
+    margin-bottom: 2rem;
+    cursor: pointer;
+    position: relative;
+
+    .cover {
+      max-width: 100%;
+      width: 17rem;
+      height: 20vh;
+      object-fit: cover;
+    }
+  }
+
+  p {
+    margin-top: 0.5rem;
+  }
+
+  .icon {
+    width: 5rem;
+    position: absolute;
+    top: 0.5rem;
+    right: 0.5rem;
+  }
+}
+
+.slider-btn {
+  width: 100%;
+  position: absolute;
+  z-index: 100;
+  top: 50vh;
+
+  img {
+    width: 100px;
+    transition: all 0.2s;
+
+    @media (max-width: 600px) {
+      width: 50px;
+    }
+  }
+
+  .prev,
+  .next {
+    position: absolute;
+    cursor: pointer;
+    border: none;
+    background-color: transparent;
+
+    &:hover {
+      img {
+        opacity: 0.7;
+      }
+    }
+  }
+
+  .prev {
+    left: 0;
+  }
+
+  .next {
+    right: 0;
+  }
+}
+
+.page-num {
+  margin: auto auto 2.2rem;
+  color: white;
+  letter-spacing: 0.2rem;
+}
+
+.content {
+  @media (max-width: 600px) {
+    min-height: 100vh;
+  }
+}
+
+.alert-item {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+
+  .v-alert {
+    background-color: var(--sub-color);
+  }
+
+  .text-warning {
+    color: var(--main-color) !important;
+  }
 }
 </style>

+ 32 - 351
src/views/Step_5.vue

@@ -1,287 +1,47 @@
 <script setup>
-import { ref, reactive, onMounted } from "vue";
 import { useMainStore } from "@/stores/store";
-import { useRouter } from "vue-router";
 import { useI18n } from "vue-i18n";
-import axios from "axios";
 import Footer from "../components/Footer.vue";
-import VuePictureCropper, { cropper } from "vue-picture-cropper";
 
 const { t } = useI18n();
-const router = useRouter();
 const store = useMainStore();
-const apiUrl = import.meta.env.VITE_API_URL;
-const imgUrl = import.meta.env.VITE_API_IMG_URL;
-
-// 測試開始
-const isShowModal = ref(false);
-const uploadInput = ref(null);
-const pic = ref("");
-const result = reactive({
-  dataURL: "",
-  // blobURL: "",
-});
-
-function selectFile(e) {
-  pic.value = "";
-  result.dataURL = "";
-  // result.blobURL = "";
-
-  // Get selected files
-  const { files } = e.target;
-  if (!files || !files.length) return;
-
-  // Convert to dataURL and pass to the cropper component
-  const file = files[0];
-  const reader = new FileReader();
-  reader.readAsDataURL(file);
-  reader.onload = () => {
-    // Update the picture source of the `img` prop
-    pic.value = String(reader.result);
-
-    // Show the modal
-    isShowModal.value = true;
-
-    // Clear selected files of input element
-    if (!uploadInput.value) return;
-    uploadInput.value.value = "";
-  };
-}
-
-async function getResult() {
-  if (!cropper) return;
-  const base64 = cropper.getDataURL();
-  const blob = await cropper.getBlob();
-  if (!blob) return;
-
-  imgFile.value = await cropper.getFile({
-    fileName: "fileName",
-  });
-
-  console.log("imgFile.value >>", imgFile.value);
-
-  result.dataURL = base64;
-  isShowModal.value = false;
-  // result.blobURL = URL.createObjectURL(blob);
-  // console.log({ base64, blob, file });
-}
-
-// 清除
-// function clear() {
-//   if (!cropper) return;
-//   cropper.clear();
-// }
-
-// 重置
-function reset() {
-  if (!cropper) return;
-  cropper.reset();
-}
-
-// 重新上傳
-function remove() {
-  file.value = null;
-  imgFile.value = null;
-  // imageUrl.value = null;
-  pic.value = "";
-  result.dataURL = "";
-}
-
 console.log("step5 store.assignBgImg", store.assignBgImg);
-
-let file = ref(null);
-// let fileInput = ref(null);
-let imgFile = ref(null);
-// let imageUrl = ref(null);
-
-// 選擇檔案
-// function onFileChange() {
-//   console.log("fileInput", fileInput.value);
-//   if (fileInput.value.files.length) {
-//     imgFile.value = fileInput.value.files[0];
-
-//     // 預覽相片
-//     const reader = new FileReader();
-//     reader.onload = () => {
-//       imageUrl.value = reader.result;
-//     };
-//     reader.readAsDataURL(imgFile.value);
-//   }
-//   console.log("imgFile.value", imgFile.value);
-// }
-
-let imgLoading = ref(false);
-
-// 算圖欄位
-let runParameters = reactive({
-  seed: "54987890",
-  denoising_strength: "0.35",
-  batch_size: "1",
-  n_iter: "30",
-});
-
-// 算圖
-async function upload() {
-  console.log("upload");
-  if (!imgFile.value) {
-    return;
-  }
-
-  store.imgPath = "";
-  imgLoading.value = true;
-  console.log("store styleNum", store.styleNum);
-
-  let url = `${apiUrl}/sd/run?seed=${runParameters.seed}&denoising_strength=${runParameters.denoising_strength}&batch_size=${runParameters.batch_size}&n_iter=${runParameters.n_iter}&style_num=${store.styleNum}`;
-
-  // 人物圖
-  const formData = new FormData();
-  formData.append("file", imgFile.value);
-
-  try {
-    let response = await axios.post(url, formData);
-    console.log("runImg", response);
-
-    if (response.status === 200) {
-      store.imgPath = response.data[0].path;
-      imgLoading.value = false;
-      console.log("store.imgPath", store.imgPath);
-
-      router.push("/step6");
-    }
-  } catch (error) {
-    console.log("error", error);
-  }
-}
-
-const openUploadInput = () => {
-  if (uploadInput.value) {
-    uploadInput.value.click();
-  }
-};
 </script>
 
 <template>
   <div class="content main-bg">
-    <v-container class="px-5 px-sm-15">
-      <div
-        v-if="imgLoading"
-        class="d-flex flex-column align-center justify-center"
-      >
-        <p class="mb-15">
-          {{ t("postcard.step5.text_1") }}<br />
-          {{ t("postcard.step5.text_2") }}
-        </p>
-
-        <v-progress-circular
-          :size="70"
-          :width="7"
-          color="white"
-          indeterminate
-        ></v-progress-circular>
-
-        <p class="mt-15">{{ t("postcard.step5.text_3") }}</p>
-      </div>
-
-      <div v-else>
-        <p class="title mb-5">{{ t("postcard.step5.text_4") }}</p>
-
-        <div class="d-flex justify-center">
-          <v-btn
-            @click="openUploadInput"
-            color="primary"
-            variant="outlined"
-            class="img-btn"
-          >
-            <span class="d-flex align-center">
-              <v-icon icon="mdi-camera" class="me-3 pt-1"> </v-icon>
-              <p>{{ t("postcard.step5.text_5") }}</p>
-            </span>
-          </v-btn>
-        </div>
-
-        <input
-          class="d-none"
-          ref="uploadInput"
-          type="file"
-          accept="image/jpg, image/jpeg, image/png"
-          @change="selectFile"
-        />
-
-        <div v-if="isShowModal" class="mt-5">
-          <!-- 尺寸 4:3 -->
-          <VuePictureCropper
-            :boxStyle="{
-              width: '100%',
-              height: '100%',
-              backgroundColor: '#f8f8f8',
-              margin: 'auto',
-            }"
-            :img="pic"
-            :options="{
-              viewMode: 1,
-              dragMode: 'crop',
-              aspectRatio: 16 / 9,
-            }"
-            :presetMode="{
-              mode: 'fixedSize',
-              width: 1024,
-              height: 768,
-            }"
-            @ready="ready"
-          />
-
-          <div class="mt-5 d-flex justify-end">
-            <v-btn @click="reset" color="grey" variant="flat" class="me-3">
-              {{ t("postcard.step5.text_6") }}
-            </v-btn>
-
-            <v-btn @click="getResult" color="primary" variant="flat">
-              {{ t("postcard.step5.text_7") }}
-            </v-btn>
-          </div>
-        </div>
-
-        <!-- Crop result preview -->
-        <section v-if="result.dataURL" class="section">
-          <div
-            class="preview-img mt-5"
-            :style="`background-image: url('${result.dataURL}')`"
-          >
-            <div
-              class="mask"
-              :style="`background-image: url('${result.dataURL}')`"
-            ></div>
-            <!-- <img :src="result.dataURL" /> -->
-          </div>
-
-          <p class="text-white mt-5">
-            將人像放置於畫面右下角<br />會得到最好看的畫面唷!
-          </p>
-        </section>
-
-        <!-- <v-file-input
-          v-model="file"
-          ref="fileInput"
-          v-on:change="onFileChange()"
-          label="選擇檔案"
-          prepend-icon="mdi-camera"
-          variant="filled"
-          class="text-white"
-        ></v-file-input>
-
-        <div class="preview-img">
-          <img class="w-100 mt-5" :src="imageUrl" alt="照片" v-if="imageUrl" />
-        </div> -->
-
-        <div class="btn-content">
-          <button @click="remove()" class="main-btn">
-            {{ t("postcard.step5.text_8") }}
-          </button>
-          <button @click="upload()" class="main-btn">{{ t("confirm") }}</button>
-        </div>
+    <v-container
+      class="px-5 px-sm-15 mt-15 d-flex flex-column align-center justify-center"
+    >
+      <div>
+        <v-img
+          max-width="500"
+          cover
+          class="my-5 mx-auto"
+          :lazy-src="`http://172.104.93.163:3219/static/assets/img/bg/${store.assignBgImg.bg_img}`"
+          :src="`http://172.104.93.163:3219/static/assets/img/bg/${store.assignBgImg.bg_img}`"
+        >
+          <template v-slot:placeholder>
+            <div class="d-flex align-center justify-center fill-height">
+              <v-progress-circular
+                color="grey-lighten-4"
+                indeterminate
+              ></v-progress-circular>
+            </div>
+          </template>
+        </v-img>
+
+        <p>{{ t(store.assignBgImg.title) }}</p>
+
+        <p
+          class="text-start px-5 my-10 description"
+          v-html="t(store.assignBgImg.description)"
+        ></p>
       </div>
 
-      <!-- <router-link to="/step6" class="main-btn">確定</router-link> -->
+      <router-link to="/step6" class="main-btn mt-auto">
+        {{ t("confirm") }}
+      </router-link>
     </v-container>
 
     <Footer url="/step4" />
@@ -289,60 +49,8 @@ const openUploadInput = () => {
 </template>
 
 <style lang="scss" scoped>
-// .mdi-camera::before {
-//   color: #fff !important;
-// }
-
-.img-btn {
-  height: auto !important;
-  padding: 10px 20px;
-  font-size: 1rem;
-  p {
-    color: var(--main-color);
-  }
-}
-
-.preview-img {
-  position: relative;
-  width: 100%;
-  height: 50vw;
-  background-repeat: no-repeat;
-  background-position: center center;
-  background-size: cover;
-
-  &::after {
-    position: absolute;
-    content: "";
-    width: 100%;
-    height: 100%;
-    background-color: rgba(0, 0, 0, 0.7);
-    z-index: 0;
-  }
-
-  .mask {
-    position: absolute;
-    left: 50%;
-    top: 50%;
-    transform: translate(-50%, -50%);
-    z-index: 15;
-    width: 100%;
-    height: 50vw;
-    background-repeat: no-repeat;
-    background-position: center center;
-    background-size: cover;
-    -webkit-clip-path: inset(25% 0% 0% 45% round 0);
-    clip-path: inset(25% 0% 0% 45% round 0);
-    overflow: hidden; // safari
-    filter: hue-rotate(0deg); // safari
-    // clip-path: xywh(40% 25% 70% 75% round 0);
-  }
-
-  // img {
-  //   width: 100%;
-  //   height: 30vh;
-  //   object-fit: cover;
-  //   position: relative;
-  // }
+.v-container {
+  min-height: 75vh;
 }
 
 .content {
@@ -352,31 +60,4 @@ const openUploadInput = () => {
   align-items: center;
   justify-content: center;
 }
-
-.btn-content {
-  width: 100%;
-  padding: 100px 10px 20px;
-  display: flex;
-  justify-content: center;
-  // position: absolute;
-  // left: 50%;
-  // bottom: 20vw;
-  // transform: translate(-50%, 0);
-
-  .main-btn {
-    margin: 10px;
-  }
-}
-
-.test {
-  width: 300px;
-  height: 500px;
-  object-fit: cover;
-}
-
-.cut {
-  width: 500px;
-  height: 500px;
-  margin: 30px auto;
-}
 </style>

+ 338 - 86
src/views/Step_6.vue

@@ -1,130 +1,382 @@
 <script setup>
-import { ref } from "vue";
-import { useI18n } from "vue-i18n";
+import { ref, reactive, onMounted } from "vue";
 import { useMainStore } from "@/stores/store";
+import { useRouter } from "vue-router";
+import { useI18n } from "vue-i18n";
 import axios from "axios";
 import Footer from "../components/Footer.vue";
+import VuePictureCropper, { cropper } from "vue-picture-cropper";
 
 const { t } = useI18n();
+const router = useRouter();
 const store = useMainStore();
+const apiUrl = import.meta.env.VITE_API_URL;
+const imgUrl = import.meta.env.VITE_API_IMG_URL;
 
-// 使用者點擊分享時帶入的資訊
-const shareData = {
-  url: store.imgPath, // 要分享的 URL
-  title: "101", // 標題
-  text: "AI明信片", // 文字內容
-};
+// 測試開始
+const isShowModal = ref(false);
+const uploadInput = ref(null);
+const pic = ref("");
+const result = reactive({
+  dataURL: "",
+  // blobURL: "",
+});
 
-console.log("shareData", shareData);
+function selectFile(e) {
+  pic.value = "";
+  result.dataURL = "";
+  // result.blobURL = "";
 
-const imageUrl = ref("");
+  // Get selected files
+  const { files } = e.target;
+  if (!files || !files.length) return;
 
-// 儲存圖片
-const downloadImage = async (url) => {
-  try {
-    const response = await axios({
-      url: url,
-      method: "GET",
-      responseType: "blob",
-    });
+  // Convert to dataURL and pass to the cropper component
+  const file = files[0];
+  const reader = new FileReader();
+  reader.readAsDataURL(file);
+  reader.onload = () => {
+    // Update the picture source of the `img` prop
+    pic.value = String(reader.result);
 
-    imageUrl.value = URL.createObjectURL(new Blob([response.data]));
+    // Show the modal
+    isShowModal.value = true;
 
-    saveImage();
-  } catch (error) {
-    console.error("Error downloading the image:", error);
+    // Clear selected files of input element
+    if (!uploadInput.value) return;
+    uploadInput.value.value = "";
+  };
+}
+
+async function getResult() {
+  if (!cropper) return;
+  const base64 = cropper.getDataURL();
+  const blob = await cropper.getBlob();
+  if (!blob) return;
+
+  imgFile.value = await cropper.getFile({
+    fileName: "fileName",
+  });
+
+  console.log("imgFile.value >>", imgFile.value);
+
+  result.dataURL = base64;
+  isShowModal.value = false;
+  // result.blobURL = URL.createObjectURL(blob);
+  // console.log({ base64, blob, file });
+}
+
+// 清除
+// function clear() {
+//   if (!cropper) return;
+//   cropper.clear();
+// }
+
+// 重置
+function reset() {
+  if (!cropper) return;
+  cropper.reset();
+}
+
+// 重新上傳
+function remove() {
+  file.value = null;
+  imgFile.value = null;
+  // imageUrl.value = null;
+  pic.value = "";
+  result.dataURL = "";
+}
+
+console.log("step5 store.assignBgImg", store.assignBgImg);
+
+let file = ref(null);
+// let fileInput = ref(null);
+let imgFile = ref(null);
+// let imageUrl = ref(null);
+
+// 選擇檔案
+// function onFileChange() {
+//   console.log("fileInput", fileInput.value);
+//   if (fileInput.value.files.length) {
+//     imgFile.value = fileInput.value.files[0];
+
+//     // 預覽相片
+//     const reader = new FileReader();
+//     reader.onload = () => {
+//       imageUrl.value = reader.result;
+//     };
+//     reader.readAsDataURL(imgFile.value);
+//   }
+//   console.log("imgFile.value", imgFile.value);
+// }
+
+let imgLoading = ref(false);
+
+// 算圖欄位
+let runParameters = reactive({
+  seed: "54987890",
+  denoising_strength: "0.35",
+  batch_size: "1",
+  n_iter: "30",
+});
+
+// 算圖
+async function upload() {
+  console.log("upload");
+  if (!imgFile.value) {
+    return;
   }
-};
 
-const saveImage = () => {
-  const a = document.createElement("a");
-  a.href = imageUrl.value;
-  a.download = "101-AI-Postcard.jpg"; // 檔案名稱
-  document.body.appendChild(a);
-  a.click();
-  document.body.removeChild(a);
-};
+  store.imgPath = "";
+  imgLoading.value = true;
+  console.log("store styleNum", store.styleNum);
+
+  let url = `${apiUrl}/sd/run?seed=${runParameters.seed}&denoising_strength=${runParameters.denoising_strength}&batch_size=${runParameters.batch_size}&n_iter=${runParameters.n_iter}&style_num=${store.styleNum}`;
+
+  // 人物圖
+  const formData = new FormData();
+  formData.append("file", imgFile.value);
 
-async function share() {
   try {
-    // 使用 Web Share API
-    await navigator.share(shareData);
-  } catch (err) {
-    // 使用者拒絕分享或發生錯誤
-    const { name, message } = err;
-    if (name === "AbortError") {
-      alert("您已取消分享此相片");
-    } else {
-      alert(err);
+    let response = await axios.post(url, formData);
+    console.log("runImg", response);
+
+    if (response.status === 200) {
+      store.imgPath = response.data[0].path;
+      imgLoading.value = false;
+      console.log("store.imgPath", store.imgPath);
+
+      router.push("/step7");
     }
+  } catch (error) {
+    console.log("error", error);
   }
-
-  // console.log("share");
-  // if (navigator.share) {
-  //   try {
-  //     await navigator.share({
-  //       title: "101",
-  //       text: "AI明信片",
-  //       url: store.imgPath,
-  //     });
-  //     console.log("分享成功");
-  //   } catch (error) {
-  //     console.error("分享失敗", error);
-  //   }
-  // } else {
-  //   alert("Web Share API 不支援在此設備上運行");
-  // }
 }
+
+const openUploadInput = () => {
+  if (uploadInput.value) {
+    uploadInput.value.click();
+  }
+};
 </script>
 
 <template>
   <div class="content main-bg">
-    <v-container class="px-5 px-sm-15 d-flex flex-column align-center">
-      <div id="result" class="mb-10 img-item">
-        <img
-          id="imageElement"
-          ref="imageElement"
-          class="w-100"
-          :src="store.imgPath"
-          alt=""
+    <v-container class="px-5 px-sm-15">
+      <div
+        v-if="imgLoading"
+        class="d-flex flex-column align-center justify-center"
+      >
+        <p class="mb-15">
+          {{ t("postcard.step5.text_1") }}<br />
+          {{ t("postcard.step5.text_2") }}
+        </p>
+
+        <v-progress-circular
+          :size="70"
+          :width="7"
+          color="white"
+          indeterminate
+        ></v-progress-circular>
+
+        <p class="mt-15">{{ t("postcard.step5.text_3") }}</p>
+      </div>
+
+      <div v-else>
+        <p class="title mb-5">{{ t("postcard.step5.text_4") }}</p>
+
+        <div class="d-flex justify-center">
+          <v-btn
+            @click="openUploadInput"
+            color="primary"
+            variant="outlined"
+            class="img-btn"
+          >
+            <span class="d-flex align-center">
+              <v-icon icon="mdi-camera" class="me-3 pt-1"> </v-icon>
+              <p>{{ t("postcard.step5.text_5") }}</p>
+            </span>
+          </v-btn>
+        </div>
+
+        <input
+          class="d-none"
+          ref="uploadInput"
+          type="file"
+          accept="image/jpg, image/jpeg, image/png"
+          @change="selectFile"
         />
-        <p>{{ t(store.assignBgImg.title) }}</p>
+
+        <div v-if="isShowModal" class="mt-5">
+          <!-- 尺寸 4:3 -->
+          <VuePictureCropper
+            :boxStyle="{
+              width: '100%',
+              height: '100%',
+              backgroundColor: '#f8f8f8',
+              margin: 'auto',
+            }"
+            :img="pic"
+            :options="{
+              viewMode: 1,
+              dragMode: 'crop',
+              aspectRatio: 16 / 9,
+            }"
+            :presetMode="{
+              mode: 'fixedSize',
+              width: 1024,
+              height: 768,
+            }"
+            @ready="ready"
+          />
+
+          <div class="mt-5 d-flex justify-end">
+            <v-btn @click="reset" color="grey" variant="flat" class="me-3">
+              {{ t("postcard.step5.text_6") }}
+            </v-btn>
+
+            <v-btn @click="getResult" color="primary" variant="flat">
+              {{ t("postcard.step5.text_7") }}
+            </v-btn>
+          </div>
+        </div>
+
+        <!-- Crop result preview -->
+        <section v-if="result.dataURL" class="section">
+          <div
+            class="preview-img mt-5"
+            :style="`background-image: url('${result.dataURL}')`"
+          >
+            <div
+              class="mask"
+              :style="`background-image: url('${result.dataURL}')`"
+            ></div>
+            <!-- <img :src="result.dataURL" /> -->
+          </div>
+
+          <p class="text-white mt-5">
+            將人像放置於畫面右下角<br />會得到最好看的畫面唷!
+          </p>
+        </section>
+
+        <!-- <v-file-input
+          v-model="file"
+          ref="fileInput"
+          v-on:change="onFileChange()"
+          label="選擇檔案"
+          prepend-icon="mdi-camera"
+          variant="filled"
+          class="text-white"
+        ></v-file-input>
+
+        <div class="preview-img">
+          <img class="w-100 mt-5" :src="imageUrl" alt="照片" v-if="imageUrl" />
+        </div> -->
+
+        <div class="btn-content">
+          <button @click="remove()" class="main-btn">
+            {{ t("postcard.step5.text_8") }}
+          </button>
+          <button @click="upload()" class="main-btn">{{ t("confirm") }}</button>
+        </div>
       </div>
 
-      <p
-        class="text-start px-5 description"
-        v-html="t(store.assignBgImg.description)"
-      ></p>
-      <!-- <button @click="share()" class="main-btn mt-15">分享相片</button> -->
-      <button @click="downloadImage(store.imgPath)" class="main-btn mt-15">
-        儲存相片
-      </button>
+      <!-- <router-link to="/step6" class="main-btn">確定</router-link> -->
     </v-container>
+
     <Footer url="/step5" />
   </div>
 </template>
 
 <style lang="scss" scoped>
+// .mdi-camera::before {
+//   color: #fff !important;
+// }
+
+.img-btn {
+  height: auto !important;
+  padding: 10px 20px;
+  font-size: 1rem;
+  p {
+    color: var(--main-color);
+  }
+}
+
+.preview-img {
+  position: relative;
+  width: 100%;
+  height: 50vw;
+  background-repeat: no-repeat;
+  background-position: center center;
+  background-size: cover;
+
+  &::after {
+    position: absolute;
+    content: "";
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0, 0, 0, 0.7);
+    z-index: 0;
+  }
+
+  .mask {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    z-index: 15;
+    width: 100%;
+    height: 50vw;
+    background-repeat: no-repeat;
+    background-position: center center;
+    background-size: cover;
+    -webkit-clip-path: inset(25% 0% 0% 45% round 0);
+    clip-path: inset(25% 0% 0% 45% round 0);
+    overflow: hidden; // safari
+    filter: hue-rotate(0deg); // safari
+    // clip-path: xywh(40% 25% 70% 75% round 0);
+  }
+
+  // img {
+  //   width: 100%;
+  //   height: 30vh;
+  //   object-fit: cover;
+  //   position: relative;
+  // }
+}
+
 .content {
-  padding: 8rem 0 8rem;
   min-height: 100vh;
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: center;
+}
 
-  .img-item {
-    img {
-      border: 8px solid white;
-    }
+.btn-content {
+  width: 100%;
+  padding: 100px 10px 20px;
+  display: flex;
+  justify-content: center;
+  // position: absolute;
+  // left: 50%;
+  // bottom: 20vw;
+  // transform: translate(-50%, 0);
 
-    p {
-      padding: 8px;
-      margin-top: -5px;
-      color: white;
-      text-shadow: none;
-      background-color: var(--main-color);
-    }
+  .main-btn {
+    margin: 10px;
   }
 }
+
+.test {
+  width: 300px;
+  height: 500px;
+  object-fit: cover;
+}
+
+.cut {
+  width: 500px;
+  height: 500px;
+  margin: 30px auto;
+}
 </style>

+ 130 - 0
src/views/Step_7.vue

@@ -0,0 +1,130 @@
+<script setup>
+import { ref } from "vue";
+import { useI18n } from "vue-i18n";
+import { useMainStore } from "@/stores/store";
+import axios from "axios";
+import Footer from "../components/Footer.vue";
+
+const { t } = useI18n();
+const store = useMainStore();
+
+// 使用者點擊分享時帶入的資訊
+const shareData = {
+  url: store.imgPath, // 要分享的 URL
+  title: "101", // 標題
+  text: "AI明信片", // 文字內容
+};
+
+console.log("shareData", shareData);
+
+const imageUrl = ref("");
+
+// 儲存圖片
+const downloadImage = async (url) => {
+  try {
+    const response = await axios({
+      url: url,
+      method: "GET",
+      responseType: "blob",
+    });
+
+    imageUrl.value = URL.createObjectURL(new Blob([response.data]));
+
+    saveImage();
+  } catch (error) {
+    console.error("Error downloading the image:", error);
+  }
+};
+
+const saveImage = () => {
+  const a = document.createElement("a");
+  a.href = imageUrl.value;
+  a.download = "101-AI-Postcard.jpg"; // 檔案名稱
+  document.body.appendChild(a);
+  a.click();
+  document.body.removeChild(a);
+};
+
+async function share() {
+  try {
+    // 使用 Web Share API
+    await navigator.share(shareData);
+  } catch (err) {
+    // 使用者拒絕分享或發生錯誤
+    const { name, message } = err;
+    if (name === "AbortError") {
+      alert("您已取消分享此相片");
+    } else {
+      alert(err);
+    }
+  }
+
+  // console.log("share");
+  // if (navigator.share) {
+  //   try {
+  //     await navigator.share({
+  //       title: "101",
+  //       text: "AI明信片",
+  //       url: store.imgPath,
+  //     });
+  //     console.log("分享成功");
+  //   } catch (error) {
+  //     console.error("分享失敗", error);
+  //   }
+  // } else {
+  //   alert("Web Share API 不支援在此設備上運行");
+  // }
+}
+</script>
+
+<template>
+  <div class="content main-bg">
+    <v-container class="px-5 px-sm-15 d-flex flex-column align-center">
+      <div id="result" class="mb-10 img-item">
+        <img
+          id="imageElement"
+          ref="imageElement"
+          class="w-100"
+          :src="store.imgPath"
+          alt=""
+        />
+        <p>{{ t(store.assignBgImg.title) }}</p>
+      </div>
+
+      <p
+        class="text-start px-5 description"
+        v-html="t(store.assignBgImg.description)"
+      ></p>
+      <!-- <button @click="share()" class="main-btn mt-15">分享相片</button> -->
+      <button @click="downloadImage(store.imgPath)" class="main-btn mt-15">
+        儲存相片
+      </button>
+    </v-container>
+    <Footer url="/step6" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.content {
+  padding: 8rem 0 8rem;
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+
+  .img-item {
+    img {
+      border: 8px solid white;
+    }
+
+    p {
+      padding: 8px;
+      margin-top: -5px;
+      color: white;
+      text-shadow: none;
+      background-color: var(--main-color);
+    }
+  }
+}
+</style>