SyuanYu 10 月之前
父節點
當前提交
4a0c40e2aa

+ 1 - 1
.env.development

@@ -1,2 +1,2 @@
-VITE_API_URL=http://localhost:5176/
+VITE_API_URL=http://192.168.192.38:8000
 VITE_API_IMG_URL=http://localhost:5176/src

+ 1 - 1
.env.production

@@ -1,2 +1,2 @@
-VITE_API_URL=http://192.168.192.38:8000/static
+VITE_API_URL=http://192.168.192.38:8000
 VITE_API_IMG_URL=http://192.168.192.38:8000/static

+ 161 - 1
package-lock.json

@@ -8,6 +8,7 @@
       "name": "ai-chatbot  ",
       "version": "0.0.0",
       "dependencies": {
+        "@mdi/font": "^7.4.47",
         "animate.css": "^4.1.1",
         "axios": "^1.6.7",
         "pinia": "^2.1.7",
@@ -17,7 +18,9 @@
       "devDependencies": {
         "@vitejs/plugin-vue": "^4.5.2",
         "sass": "^1.70.0",
-        "vite": "^5.0.10"
+        "vite": "^5.0.10",
+        "vite-plugin-vuetify": "^2.0.3",
+        "vuetify": "^3.6.3"
       }
     },
     "node_modules/@babel/parser": {
@@ -404,6 +407,11 @@
       "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
       "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
     },
+    "node_modules/@mdi/font": {
+      "version": "7.4.47",
+      "resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz",
+      "integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw=="
+    },
     "node_modules/@rollup/rollup-android-arm-eabi": {
       "version": "4.9.6",
       "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz",
@@ -687,6 +695,19 @@
       "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
       "integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g=="
     },
+    "node_modules/@vuetify/loader-shared": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@vuetify/loader-shared/-/loader-shared-2.0.3.tgz",
+      "integrity": "sha512-Ss3GC7eJYkp2SF6xVzsT7FAruEmdihmn4OCk2+UocREerlXKWgOKKzTN5PN3ZVN5q05jHHrsNhTuWbhN61Bpdg==",
+      "dev": true,
+      "dependencies": {
+        "upath": "^2.0.1"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0",
+        "vuetify": "^3.0.0"
+      }
+    },
     "node_modules/animate.css": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",
@@ -784,6 +805,23 @@
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
     },
+    "node_modules/debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dev": true,
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/delayed-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -994,6 +1032,12 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+      "dev": true
+    },
     "node_modules/nanoid": {
       "version": "3.3.7",
       "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
@@ -1200,6 +1244,16 @@
         "node": ">=8.0"
       }
     },
+    "node_modules/upath": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz",
+      "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==",
+      "dev": true,
+      "engines": {
+        "node": ">=4",
+        "yarn": "*"
+      }
+    },
     "node_modules/vite": {
       "version": "5.0.12",
       "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
@@ -1255,6 +1309,25 @@
         }
       }
     },
+    "node_modules/vite-plugin-vuetify": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-2.0.3.tgz",
+      "integrity": "sha512-HbYajgGgb/noaVKNRhnnXIiQZrNXfNIeanUGAwXgOxL6h/KULS40Uf51Kyz8hNmdegF+DwjgXXI/8J1PNS83xw==",
+      "dev": true,
+      "dependencies": {
+        "@vuetify/loader-shared": "^2.0.3",
+        "debug": "^4.3.3",
+        "upath": "^2.0.1"
+      },
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "peerDependencies": {
+        "vite": ">=5",
+        "vue": "^3.0.0",
+        "vuetify": "^3.0.0"
+      }
+    },
     "node_modules/vue": {
       "version": "3.4.15",
       "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.15.tgz",
@@ -1288,6 +1361,40 @@
       "peerDependencies": {
         "vue": "^3.2.0"
       }
+    },
+    "node_modules/vuetify": {
+      "version": "3.6.3",
+      "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.6.3.tgz",
+      "integrity": "sha512-OBYYJYnNeUYA7kwrv8Rag1EBFbGWAQxJpp0s98U2KQ6SPU7MzzcrvNn7t69vcDbj7mR7Dcf9/jvFapfranXZvA==",
+      "dev": true,
+      "engines": {
+        "node": "^12.20 || >=14.13"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/johnleider"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.7",
+        "vite-plugin-vuetify": ">=1.0.0",
+        "vue": "^3.3.0",
+        "vue-i18n": "^9.0.0",
+        "webpack-plugin-vuetify": ">=2.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        },
+        "vite-plugin-vuetify": {
+          "optional": true
+        },
+        "vue-i18n": {
+          "optional": true
+        },
+        "webpack-plugin-vuetify": {
+          "optional": true
+        }
+      }
     }
   },
   "dependencies": {
@@ -1462,6 +1569,11 @@
       "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
       "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
     },
+    "@mdi/font": {
+      "version": "7.4.47",
+      "resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz",
+      "integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw=="
+    },
     "@rollup/rollup-android-arm-eabi": {
       "version": "4.9.6",
       "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz",
@@ -1658,6 +1770,15 @@
       "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz",
       "integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g=="
     },
+    "@vuetify/loader-shared": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@vuetify/loader-shared/-/loader-shared-2.0.3.tgz",
+      "integrity": "sha512-Ss3GC7eJYkp2SF6xVzsT7FAruEmdihmn4OCk2+UocREerlXKWgOKKzTN5PN3ZVN5q05jHHrsNhTuWbhN61Bpdg==",
+      "dev": true,
+      "requires": {
+        "upath": "^2.0.1"
+      }
+    },
     "animate.css": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",
@@ -1732,6 +1853,15 @@
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
     },
+    "debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dev": true,
+      "requires": {
+        "ms": "2.1.2"
+      }
+    },
     "delayed-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -1875,6 +2005,12 @@
         "mime-db": "1.52.0"
       }
     },
+    "ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+      "dev": true
+    },
     "nanoid": {
       "version": "3.3.7",
       "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
@@ -1986,6 +2122,12 @@
         "is-number": "^7.0.0"
       }
     },
+    "upath": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz",
+      "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==",
+      "dev": true
+    },
     "vite": {
       "version": "5.0.12",
       "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
@@ -1998,6 +2140,17 @@
         "rollup": "^4.2.0"
       }
     },
+    "vite-plugin-vuetify": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-2.0.3.tgz",
+      "integrity": "sha512-HbYajgGgb/noaVKNRhnnXIiQZrNXfNIeanUGAwXgOxL6h/KULS40Uf51Kyz8hNmdegF+DwjgXXI/8J1PNS83xw==",
+      "dev": true,
+      "requires": {
+        "@vuetify/loader-shared": "^2.0.3",
+        "debug": "^4.3.3",
+        "upath": "^2.0.1"
+      }
+    },
     "vue": {
       "version": "3.4.15",
       "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.15.tgz",
@@ -2017,6 +2170,13 @@
       "requires": {
         "@vue/devtools-api": "^6.5.0"
       }
+    },
+    "vuetify": {
+      "version": "3.6.3",
+      "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.6.3.tgz",
+      "integrity": "sha512-OBYYJYnNeUYA7kwrv8Rag1EBFbGWAQxJpp0s98U2KQ6SPU7MzzcrvNn7t69vcDbj7mR7Dcf9/jvFapfranXZvA==",
+      "dev": true,
+      "requires": {}
     }
   }
 }

+ 4 - 1
package.json

@@ -9,6 +9,7 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "@mdi/font": "^7.4.47",
     "animate.css": "^4.1.1",
     "axios": "^1.6.7",
     "pinia": "^2.1.7",
@@ -18,6 +19,8 @@
   "devDependencies": {
     "@vitejs/plugin-vue": "^4.5.2",
     "sass": "^1.70.0",
-    "vite": "^5.0.10"
+    "vite": "^5.0.10",
+    "vite-plugin-vuetify": "^2.0.3",
+    "vuetify": "^3.6.3"
   }
 }

二進制
src/assets/img/background.webp


二進制
src/assets/img/skylentern.png


二進制
src/assets/img/tandan.webp


+ 4 - 7
src/components/Footer.vue

@@ -1,15 +1,12 @@
 <script setup>
-import { ref, reactive } from "vue";
-const props = defineProps(['url'])
-
-console.log(props.url)
-
+const props = defineProps(["url", "back"]);
 </script>
 
 <template>
   <footer>
     <router-link :to="props.url">上一步</router-link>
-    <router-link to="/">回到首頁</router-link>
+    <router-link :to="props.back ? props.back : '/'">回到首頁</router-link>
+    <!-- <router-link to="/">回到首頁</router-link> -->
   </footer>
 </template>
 
@@ -23,7 +20,7 @@ footer {
   bottom: 0;
 
   a {
-    color: #B3B3B3;
+    color: #b3b3b3;
     letter-spacing: 2px;
     text-decoration: none;
   }

+ 2 - 6
src/components/Language.vue

@@ -1,14 +1,11 @@
 <script setup>
-import { ref, reactive } from "vue";
+import { reactive } from "vue";
 import { useRouter } from "vue-router";
-import Marquee from "../components/Marquee.vue";
 
 const router = useRouter();
-let isLanguagePage = ref(true);
 
 function chooseLang(lang) {
   console.log("選擇語言:", lang);
-  isLanguagePage.value = false;
   localStorage.setItem("lang", lang);
   router.push("/step1");
 }
@@ -34,7 +31,6 @@ let langList = reactive([
 </script>
 
 <template>
-  <Marquee />
   <div class="lang-content">
     <div style="position: relative">
       <section>
@@ -83,7 +79,7 @@ let langList = reactive([
     font-size: 1.625rem;
 
     @media (max-width: 600px) {
-      font-size: 1.25rem;
+      font-size: 1.5rem;
     }
   }
 

+ 11 - 6
src/components/Marquee.vue

@@ -72,11 +72,11 @@
 }
 
 #marquee {
-  margin-top: 50px;
   width: 100%;
-  position: absolute;
-  top: 50%;
-  transform: translate(0, -50%);
+  // margin-top: 50px;
+  // position: absolute;
+  // top: 50%;
+  // transform: translate(0, -50%);
 
   .view {
     overflow: hidden;
@@ -101,13 +101,18 @@
   }
 
   img {
-    // width: 100%;
+    width: 100%;
     height: 320px;
     object-fit: cover;
     vertical-align: middle;
 
     @media (max-width: 768px) {
-      height: 35vh;
+      height: 30vh;
+      width: 400px;
+    }
+
+    @media (max-width: 375px) {
+      height: 25vh;
     }
   }
 }

+ 2 - 0
src/main.js

@@ -5,10 +5,12 @@ import { createPinia } from 'pinia'
 
 import App from './App.vue'
 import router from './router'
+import vuetify from './plugins/vuetify'
 
 const app = createApp(App)
 
 app.use(createPinia())
 app.use(router)
+app.use(vuetify)
 
 app.mount('#app')

+ 29 - 0
src/plugins/vuetify.js

@@ -0,0 +1,29 @@
+import 'vuetify/styles'
+import { createVuetify } from 'vuetify'
+import * as components from 'vuetify/components'
+import * as directives from 'vuetify/directives'
+import { aliases, mdi } from 'vuetify/iconsets/mdi'
+import '@mdi/font/css/materialdesignicons.css'
+
+export default createVuetify({
+  components,
+  directives,
+  icons: {
+    defaultSet: 'mdi',
+    aliases,
+    sets: {
+      mdi,
+    },
+  },
+  theme: {
+    themes: {
+      light: {
+        dark: false,
+        colors: {
+          primary: '#B78C5F',
+          brown: '#231816',
+        },
+      },
+    },
+  },
+})

+ 36 - 0
src/router/index.js

@@ -4,6 +4,13 @@ import Language from "../components/Language.vue";
 import Step_1 from "../views/Step_1.vue";
 import Step_2 from "../views/Step_2.vue";
 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 Interact from '../views/Interact.vue'
+ import InteractStep_1 from '../views/InteractStep_1.vue'
+ import InteractStep_2 from '../views/InteractStep_2.vue'
+ import InteractHome from '../views/InteractHome.vue'
 
 const router = createRouter({
   history: createWebHashHistory(),
@@ -29,6 +36,35 @@ const router = createRouter({
           path: 'step3',
           component: Step_3,
         },
+        {
+          path: 'step4',
+          component: Step_4,
+        },
+        {
+          path: 'step5',
+          component: Step_5,
+        },
+        // 天燈
+        {
+          path: '/interact',
+          name: 'Interact',
+          component: Interact
+        },
+        {
+          path: '/interact_step1',
+          name: 'Interact_step1',
+          component: InteractStep_1
+        },
+        {
+          path: '/interact_step2',
+          name: 'Interact_step2',
+          component: InteractStep_2
+        },
+        {
+          path: '/interacthome',
+          name: 'Interact_home',
+          component: InteractHome
+        }
       ],
     },
   ]

+ 3 - 0
src/stores/store.js

@@ -2,6 +2,9 @@ import { defineStore } from 'pinia';
 
 export const useMainStore = defineStore('mainStore', {
     state: () => ({
+        assignBgImg: "",
+        styleNum: null,
+        imgPath: "",
     }),
     getters: {
     },

+ 44 - 3
src/views/HomeView.vue

@@ -31,9 +31,12 @@ p {
   padding: 4rem 0 6rem;
   position: relative;
   background-color: var(--sub-color);
-  // display: flex;
-  // flex-direction: column;
-  // justify-content: center;
+}
+
+.main-bg {
+  background-image: url("../assets/img/background.webp");
+  background-position: center;
+  background-size: cover;
 }
 
 .main-btn {
@@ -51,11 +54,49 @@ p {
   text-align: center;
   cursor: pointer;
   transition: all 0.3s;
+
   &:hover {
     opacity: 0.8;
   }
+
   &:last-child {
     margin-bottom: 0;
   }
+
+  @media (max-width: 600px) {
+    font-size: 1.25rem;
+  }
+}
+
+.title {
+  padding-top: 4rem;
+  margin-bottom: 2rem;
+  font-size: 1.625rem;
+
+  @media (max-width: 600px) {
+    padding-top: 2rem;
+    font-size: 1.25rem;
+  }
+}
+
+.lartern-content {
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  background-blend-mode: multiply;
+  background-image: url("@/assets/img/tandan.webp");
+  background-size: cover;
+  background-repeat: no-repeat;
+  background-position: 85% 50%;
+  
+  p {
+    line-height: 1.7;
+    letter-spacing: 2px;
+    text-align: center;
+    color: #b3b3b4;
+    text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
+  }
 }
 </style>

+ 27 - 0
src/views/Interact.vue

@@ -0,0 +1,27 @@
+<script setup>
+import Footer from "../components/Footer.vue";
+</script>
+
+<template>
+  <div class="lartern-content">
+    <h3 class="title">
+      在這裡 <br />
+      選一顆天燈 <br />
+      寫下你的祝福或願望吧
+    </h3>
+
+    <p>
+      您傳遞出去的願望 <br />
+      會在101觀景台-5F購票處的 <br />
+      環景螢幕牆旁的螢幕定時播放
+    </p>
+
+    <router-link to="/interact_step1" class="main-btn mt-15">
+      下一步
+    </router-link>
+  </div>
+
+  <Footer url="/interacthome" back="/interacthome" />
+</template>
+
+<style lang="scss" scoped></style>

+ 54 - 0
src/views/InteractHome.vue

@@ -0,0 +1,54 @@
+<script setup>
+import { reactive } from "vue";
+import { useRouter } from "vue-router";
+let router = useRouter();
+
+function chooseLang(lang) {
+  console.log("選擇語言:", lang);
+  localStorage.setItem("lang", lang);
+  router.push("/interact");
+}
+
+let langList = reactive([
+  {
+    lang: "中文",
+    value: "zh-tw",
+  },
+  {
+    lang: "English",
+    value: "en-us",
+  },
+  {
+    lang: "日本語",
+    value: "ja-jp",
+  },
+  {
+    lang: "한국어",
+    value: "ko-kr",
+  },
+]);
+</script>
+
+<template>
+  <div class="lang-content">
+    <div style="position: relative">
+      <section>
+        <p>請選擇您的語言</p>
+      </section>
+      <button
+        v-for="(item, index) in langList"
+        :key="index"
+        @click="chooseLang(item.value)"
+        class="main-btn"
+      >
+        {{ item.lang }}
+      </button>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.lang-content {
+  background-image: url("@/assets/img/tandan.webp");
+}
+</style>

+ 51 - 0
src/views/InteractStep_1.vue

@@ -0,0 +1,51 @@
+<script setup>
+import Footer from "../components/Footer.vue";
+</script>
+
+<template>
+  <div class="lartern-content">
+    <section>
+      <h3>天燈的起源</h3>
+      <p>
+        「天燈」起初是為了傳遞訊息之用,但目前通常則被當成節慶折福許願的工具,象徵收穫的成功和幸福每一年。許多旅遊景點每天晚上都提供放天燈的活動
+      </p>
+    </section>
+
+    <section class="my-5">
+      <h3>祈福目的</h3>
+      <p>健康|平安&nbsp;&nbsp;&nbsp;幸福|快樂&nbsp;&nbsp;&nbsp;前途|事業</p>
+      <p>愛情|婚姻&nbsp;&nbsp;&nbsp;金錢|發財&nbsp;&nbsp;&nbsp;考試|讀書</p>
+    </section>
+
+    <section>
+      <h3>環保天燈</h3>
+      <p>
+        天燈是鐵絲或竹子,紙、油紙組成。當天燈的燃料燒完後,會掉落附近地面,餘火會燒到附近住家的屋頂或菜園、甚至引發森林大火,並且材料會殘留重金屬,可能對生態鏈造成嚴重危害。於是2018年起台灣研 發出了能在空中徹底燃燒成灰的 <br />
+        [全紙環保天燈] <br />
+        當然,在101放天燈,也非常環保
+      </p>
+    </section>
+
+    <router-link to="/interact_step2" class="main-btn mt-10">
+      下一步
+    </router-link>
+  </div>
+
+  <Footer url="/interact" back="/interacthome" />
+</template>
+
+<style lang="scss" scoped>
+.lartern-content {
+  padding: 4rem 10vw 0;
+  background-image: url("@/assets/img/background.webp");
+
+  h3 {
+    margin-bottom: 10px;
+    font-size: 24px;
+    line-height: 1.5;
+    text-align: center;
+    color: #fff;
+    text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
+  }
+}
+</style>

+ 84 - 0
src/views/InteractStep_2.vue

@@ -0,0 +1,84 @@
+<script setup>
+import { ref } from "vue";
+import axios from "axios";
+import Footer from "../components/Footer.vue";
+
+let loading = ref(false);
+let inputValue = ref("");
+let skyImageUrl = ref("");
+
+function getValue() {
+  loading.value = true;
+  axios
+    .post(`https://cmm.ai:9101/tendentest?client_message=${inputValue.value}`)
+    .then((response) => {
+      loading.value = false;
+      skyImageUrl.value = response.data.url;
+    })
+    .catch((error) => {
+      loading.value = false;
+      console.log(error);
+    });
+}
+</script>
+
+<template>
+  <div class="lartern-content px-10">
+    <div class="lartern-item">
+      <div v-if="loading">
+        <v-progress-circular
+          :size="70"
+          :width="7"
+          color="white"
+          indeterminate
+        ></v-progress-circular>
+      </div>
+
+      <img
+        v-else-if="skyImageUrl === ''"
+        src="/src/assets/img/skylentern.png"
+        alt=""
+        class="skyLarten"
+      />
+
+      <img
+        v-else
+        :src="`https://cmm.ai:9101/${skyImageUrl}`"
+        class="skyLarten"
+        alt=""
+      />
+    </div>
+
+    <h3 class="title">寫下你的願望</h3>
+    <input v-model="inputValue" type="text" />
+    <div class="d-flex flex-column align-center mt-15">
+      <button @click="getValue()" class="main-btn mb-5">預覽</button>
+      <button class="main-btn">送出</button>
+    </div>
+  </div>
+
+  <Footer url="/interact_step1" back="/interacthome" />
+</template>
+
+<style lang="scss" scoped>
+.lartern-content {
+  input {
+    padding: 10px;
+    background-color: transparent;
+    color: #fff;
+    border: 2px solid var(--main-color);
+    border-radius: 10px;
+  }
+
+  .lartern-item {
+    height: 40vh;
+    display: flex;
+    align-items: center;
+
+    img {
+      max-width: 300px;
+      height: auto;
+    }
+  }
+}
+</style>

+ 27 - 113
src/views/Step_1.vue

@@ -1,62 +1,37 @@
 <script setup>
+import { ref, reactive } from "vue";
 import "animate.css";
-import Footer from "../components/Footer.vue";
+import axios from "axios";
+import Marquee from "../components/Marquee.vue";
 </script>
 
 <template>
   <div class="content">
-    <ul class="step-list">
-      <li>
-        <img class="num" src="../assets/img/step-1.png" alt="" />
-        <p>尋找一面乾淨的白牆</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>選擇背景</p>
-          <small>20 款以上景點照片挑選</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>手機前後鏡頭拍攝</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>獲得照片!</p>
-          <small>馬上分享給朋友</small>
-        </div>
-      </li>
-    </ul>
+    <section class="px-3 mb-10">
+      <p>
+        AI 明信片可以分辨來自不同國籍的旅客 <br />
+        只需要使用手機自拍,加上 AI 技術 <br />
+        將您完美合成至台灣各景點
+      </p>
+      <p>
+        如果您此趟旅程沒辦法每個地方都去過 <br />
+        那這是我們送給您的禮物
+      </p>
+    </section>
+
+    <Marquee />
 
     <router-link to="/step2" class="main-btn">下一步</router-link>
-
-    <Footer url="/" />
+    <div class="hashtag">
+      <span># 認識台灣</span>
+      <span># 馬上取得照片</span>
+    </div>
   </div>
 </template>
 
 <style lang="scss" scoped>
 p {
-  font-size: 1.125rem;
+  font-size: 1.5rem;
 
   @media (max-width: 600px) {
     font-size: 1rem;
@@ -64,80 +39,19 @@ p {
 }
 
 span {
-  font-size: 1rem;
+  font-size: 1.25rem;
 
   @media (max-width: 600px) {
-    font-size: 0.75rem;
+    font-size: 1rem;
   }
 }
 
-.step-list {
-  padding: 0;
-  margin-top: 4rem;
+.content {
+  height: 100vh;
+  padding: 4rem 0 0;
   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 {
@@ -146,7 +60,7 @@ span {
   }
 
   .main-btn {
-    margin: 3rem auto 0;
+    margin: 3rem auto;
   }
 
   .hashtag {

+ 111 - 318
src/views/Step_2.vue

@@ -1,371 +1,164 @@
 <script setup>
-import { ref, reactive, computed } from "vue";
-import { useMainStore } from "@/stores/store";
 import "animate.css";
-import axios from "axios";
 import Footer from "../components/Footer.vue";
-
-const store = useMainStore();
-let bgImg = reactive({
-  list: [],
-});
-
-let assignBgImg = ref("");
-
-function handleBgImg(name) {
-  console.log("name", name);
-  assignBgImg.value = name;
-}
-
-async function getBgImgNames() {
-  let url = "http://172.104.93.163:3219/sd/bg_img_names";
-
-  try {
-    let response = await axios.get(url);
-    console.log("getBgImgNames", response.data);
-    response.data.map((item) => bgImg.list.push(item));
-    console.log("bgImg.list", bgImg.list);
-  } catch (error) {
-    console.log("error", error);
-  }
-}
-
-getBgImgNames();
-
-const currentPhotos = computed(() => {
-  const start = currentIndex.value;
-  const end = start + perPage.value;
-  return bgImg.list.slice(start, end);
-});
-
-console.log("currentPhotos", currentPhotos);
-
-let currentIndex = ref(0);
-let perPage = ref(2);
-
-function prev() {
-  console.log("prev");
-  console.log("1 > currentIndex.value", currentIndex.value);
-  if (currentIndex.value > 0) {
-    currentIndex.value -= perPage.value;
-  }
-  console.log("2 > currentIndex.value", currentIndex.value);
-  console.log("perPage.value", perPage.value);
-}
-
-function next() {
-  console.log("next");
-  if (currentIndex.value + perPage.value < bgImg.list.length) {
-    currentIndex.value += perPage.value;
-  }
-  console.log("2 > currentIndex.value", currentIndex.value);
-  console.log("perPage.value", perPage.value);
-}
-
-// 計算頁數
-const totalPages = computed(() => Math.ceil(bgImg.list.length / perPage.value));
-
-const currentPage = computed(
-  () => Math.floor(currentIndex.value / perPage.value) + 1
-);
-
-// 測試欄位
-let parameters = reactive({
-  styel_name: "",
-  prompt: "",
-  negative_prompt: "",
-  bg_img: "",
-  styles: ["real"],
-});
-
-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 apiUrl = import.meta.env.VITE_API_IMG_URL;
-console.log("VITE_API_URL", apiUrl);
-async function setParameters() {
-  let url = "http://172.104.93.163:3219/sd/paprameter";
-  let getUrl = "http://172.104.93.163:3219/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 = `http://172.104.93.163:3219/sd/run?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);
-  }
-}
 </script>
 
 <template>
   <div class="content">
-    <p class="title">選擇背景</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"
-      >
-        <img class="cover" :src="`${apiUrl}/assets/img/bg/${item}`" alt="" />
-        <p>{{ item.replace(".png", "") }}</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>
-
-      <span class="page-num">{{ currentPage }} / {{ totalPages }}</span>
-
-      <!-- 測試 -->
-      <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="file">上傳人物照片</label>
-          <input
-            ref="fileInput"
-            v-on:change="onFileChange()"
-            type="file"
-            id="file"
-          />
-        </form>
+    <ul class="step-list">
+      <li>
+        <img class="num" src="../assets/img/step-1.png" alt="" />
+        <p>尋找一面乾淨的白牆</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>選擇背景</p>
+          <small>20 款以上景點照片挑選</small>
+        </div>
+      </li>
 
-        <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>
+      <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>手機前後鏡頭拍攝</p>
+          <small>人臉對準綠色框框</small>
+        </div>
+      </li>
 
-        <div v-if="imgPath !== ''">
-          <a :href="imgPath" target="_blank">查看算圖結果</a>
+      <li>
+        <div class="img-box">
+          <img class="cover" src="../assets/img/step/3.png" alt="" />
         </div>
-      </div>
+        <div class="step">
+          <img class="num" src="../assets/img/step-4.png" alt="" />
+          <p>獲得照片!</p>
+          <small>馬上分享給朋友</small>
+        </div>
+      </li>
+    </ul>
 
-      <!-- <router-link to="/step3" class="main-btn">下一步</router-link> -->
-    </div>
+    <router-link to="/step3" class="main-btn">下一步</router-link>
 
     <Footer url="/step1" />
   </div>
 </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;
-  }
+p {
+  font-size: 1.125rem;
 
-  .spinner {
-    width: 70px;
-    margin-bottom: -10px;
-  }
-
-  a,
-  #file {
-    color: white;
-  }
-
-  a {
-    text-align: center;
-    display: block;
-    margin-top: 10px;
-  }
-
-  .main-btn {
-    margin: auto;
+  @media (max-width: 600px) {
+    font-size: 1rem;
   }
 }
 
-img {
-  width: 100%;
-}
-
-.title {
-  padding-top: 4rem;
-  margin-bottom: 2rem;
-  font-size: 1.625rem;
+span {
+  font-size: 1rem;
 
   @media (max-width: 600px) {
-    padding-top: 2rem;
-    font-size: 1.25rem;
+    font-size: 0.75rem;
   }
 }
 
-.img-content {
-  // height: 80vh;
-  padding: 0 2rem;
+.step-list {
+  padding: 0;
+  margin-top: 4rem;
   display: flex;
   flex-direction: column;
   align-items: center;
+  justify-content: center;
+  list-style: none;
 
-  @media (max-width: 600px) {
-    height: 100%;
-    padding: 0 4rem;
+  .num {
+    width: 2rem;
+    margin-bottom: 1rem;
   }
 
-  .bg-img {
-    margin-bottom: 2rem;
-    cursor: pointer;
-    position: relative;
-
-    .cover {
-      max-width: 100%;
-      width: 30rem;
-      height: 25vh;
-      object-fit: cover;
-    }
+  .arrow {
+    width: 3rem;
+    margin-top: 1rem;
   }
 
-  p {
-    margin-top: 0.5rem;
-  }
+  li {
+    max-width: 500px;
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 1.2rem;
 
-  .icon {
-    width: 5rem;
-    position: absolute;
-    top: 0.5rem;
-    right: 0.5rem;
-  }
-}
+    &:first-child {
+      align-items: center;
+      flex-direction: column;
+    }
 
-.slider-btn {
-  width: 100%;
-  position: absolute;
-  z-index: 100;
-  top: 50vh;
+    .step {
+      width: 15rem;
+      padding-top: 1.5rem;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
 
-  img {
-    width: 100px;
-    transition: all 0.2s;
+      @media (max-width: 600px) {
+        max-width: 10.5rem;
+        padding-top: 0;
+      }
+    }
 
-    @media (max-width: 600px) {
-      width: 50px;
+    small {
+      margin-top: 0.3rem;
+      color: white;
+      letter-spacing: 2px;
     }
-  }
 
-  .prev,
-  .next {
-    position: absolute;
-    cursor: pointer;
-    border: none;
-    background-color: transparent;
+    .img-box {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      margin-right: 1.5rem;
+
+      .cover {
+        max-width: 15rem;
 
-    &:hover {
-      img {
-        opacity: 0.7;
+        @media (max-width: 600px) {
+          max-width: 9rem;
+        }
       }
     }
   }
+}
 
-  .prev {
-    left: 0;
+.content {
+
+  p {
+    &:first-child {
+      margin-bottom: 1.625rem;
+    }
   }
 
-  .next {
-    right: 0;
+  .main-btn {
+    margin: 3rem auto 0;
   }
-}
 
-.page-num {
-  margin: auto auto 2.2rem;
-  color: white;
-  letter-spacing: 0.2rem;
-}
+  .hashtag {
+    display: flex;
+    justify-content: center;
 
-.content {
-  @media (max-width: 600px) {
-    min-height: 100vh;
+    span {
+      color: white;
+      &:last-child {
+        margin-left: 20px;
+      }
+    }
   }
 }
 </style>

+ 463 - 37
src/views/Step_3.vue

@@ -1,68 +1,494 @@
 <script setup>
-import { ref, reactive } from "vue";
+import { ref, reactive, computed, onMounted } from "vue";
+import { useMainStore } from "@/stores/store";
+import { useRouter } from "vue-router";
 import "animate.css";
 import axios from "axios";
+import Footer from "../components/Footer.vue";
+
+const router = useRouter();
+const store = useMainStore();
+const apiUrl = import.meta.env.VITE_API_URL;
+const imgUrl = import.meta.env.VITE_API_IMG_URL;
+console.log("VITE_API_URL", apiUrl);
+
+let bgImg = reactive({
+  list: [],
+});
+
+let assignBgImg = ref("");
+
+function handleBgImg(item) {
+  console.log("name", item);
+  assignBgImg.value = item;
+  store.assignBgImg = item;
+
+  parameter.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);
+}
+
+// async function getBgImgNames() {
+//   let url = `${apiUrl}/sd/bg_img_names`;
+
+//   try {
+//     let response = await axios.get(url);
+//     console.log("getBgImgNames", response.data);
+//     response.data.map((item) => bgImg.list.push(item));
+//     console.log("bgImg.list", bgImg.list);
+//   } catch (error) {
+//     console.log("error", error);
+//   }
+// }
+
+onMounted(() => {
+  // getBgImgNames();
+  getParameters();
+});
+
+const currentPhotos = computed(() => {
+  const start = currentIndex.value;
+  const end = start + perPage.value;
+  return parameter.value.slice(start, end);
+  // return bgImg.list.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 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 parameter = ref([]);
+
+async function getParameters() {
+  let url = `${apiUrl}/sd/parameters`;
+
+  try {
+    let response = await axios.get(url);
+    console.log("getParameters", response);
+    console.log("response", response);
+    parameter.value = response.data;
+    console.log("parameter.list", parameter.value);
+  } catch (error) {
+    console.log("error", error);
+  }
+}
+
+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() {
+  if (store.assignBgImg && store.assignBgImg !== "") {
+    alertShow.value = false;
+    router.push("/step4");
+  } else {
+    alertShow.value = true;
+    setTimeout(() => {
+      alertShow.value = false;
+    }, 2000);
+  }
+}
 </script>
 
 <template>
   <div class="content">
-    <p>
-      AI 明信片可以分辨來自不同國籍的旅客 <br />
-      只需要使用手機自拍,加上 AI 技術 <br />
-      將您完美合成至台灣各景點
-    </p>
-    <p>
-      如果您此趟旅程沒辦法每個地方都去過 <br />
-      那這是我們送給您的禮物
-    </p>
-    <router-link to="/step2" class="main-btn">下一步</router-link>
-    <div class="hashtag">
-      <span># 認識台灣</span>
-      <span># 馬上取得照片</span>
+    <p class="title">選擇背景</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"
+      >
+        <img
+          class="cover"
+          :src="`http://192.168.192.38:8000/static/assets/img/bg/${item.bg_img}`"
+          alt=""
+        />
+        <!-- {{ bg_img }} -->
+        <p>{{ item.bg_img.replace(".png", "") }}</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>
+
+      <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">下一步</a>
+
+      <!-- <router-link to="/step4" class="main-btn">下一步</router-link> -->
+
+      <div v-if="alertShow" class="alert-item">
+        <v-alert border="top" type="warning" variant="outlined" class="mt-5">
+          尚未選擇背景
+        </v-alert>
+      </div>
     </div>
+
+    <Footer url="/step2" />
   </div>
 </template>
 
 <style lang="scss" scoped>
-p {
-  font-size: 1.5rem;
+// .test-box {
+//   form {
+//     display: flex;
+//     flex-direction: column;
+//     margin-bottom: 40px;
+//   }
+//   label {
+//     color: white;
+//     margin-bottom: 5px;
+//     letter-spacing: 1px;
+//   }
 
-  @media (max-width: 600px) {
-    font-size: 1rem;
-  }
+//   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%;
 }
 
-span {
-  font-size: 1.25rem;
+// .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;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
 
   @media (max-width: 600px) {
-    font-size: 1rem;
+    height: 100%;
+    padding: 0 4rem;
   }
-}
 
-.content {
-  height: 100vh;
+  .bg-img {
+    margin-bottom: 2rem;
+    cursor: pointer;
+    position: relative;
 
-  p {
-    &:first-child {
-      margin-bottom: 1.625rem;
+    .cover {
+      max-width: 100%;
+      width: 30rem;
+      height: 25vh;
+      object-fit: cover;
     }
   }
 
-  .main-btn {
-    margin: 3rem auto;
+  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;
+    }
   }
 
-  .hashtag {
-    display: flex;
-    justify-content: center;
+  .prev,
+  .next {
+    position: absolute;
+    cursor: pointer;
+    border: none;
+    background-color: transparent;
 
-    span {
-      color: white;
-      &:last-child {
-        margin-left: 20px;
+    &: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>

+ 135 - 0
src/views/Step_4.vue

@@ -0,0 +1,135 @@
+<script setup>
+import { ref, reactive } from "vue";
+import { useMainStore } from "@/stores/store";
+import { useRouter } from "vue-router";
+import axios from "axios";
+import Footer from "../components/Footer.vue";
+
+const router = useRouter();
+const store = useMainStore();
+const apiUrl = import.meta.env.VITE_API_URL;
+const imgUrl = import.meta.env.VITE_API_IMG_URL;
+
+console.log("Step4 store.assignBgImg", store.assignBgImg);
+
+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 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("/step5");
+    }
+  } catch (error) {
+    console.log("error", error);
+  }
+}
+</script>
+
+<template>
+  <div class="content main-bg">
+    <v-container class="px-5 px-sm-15">
+      <div v-if="imgLoading" class="d-flex justify-center">
+        <v-progress-circular
+          :size="70"
+          :width="7"
+          color="white"
+          indeterminate
+        ></v-progress-circular>
+      </div>
+
+      <div v-else>
+        <p class="title mb-5">請上傳您的相片</p>
+
+        <v-file-input
+          ref="fileInput"
+          v-on:change="onFileChange()"
+          label="選擇檔案"
+          prepend-icon="mdi-camera"
+          variant="filled"
+          class="text-white mb-15"
+        ></v-file-input>
+      </div>
+
+      <!-- <router-link to="/step5" class="main-btn">確定</router-link> -->
+
+      <div class="btn-content">
+        <router-link to="/step5" class="main-btn">重新上傳</router-link>
+        <button @click="upload()" class="main-btn">確定</button>
+      </div>
+    </v-container>
+
+    <Footer url="/step3" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.mdi-camera::before {
+  color: #fff !important;
+}
+
+.content {
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.btn-content {
+  width: 100%;
+  padding: 0 20px;
+  display: flex;
+  justify-content: center;
+  position: absolute;
+  left: 50%;
+  bottom: 20vw;
+  transform: translate(-50%, 0);
+
+  .main-btn {
+    margin: 10px;
+  }
+}
+</style>

+ 28 - 0
src/views/Step_5.vue

@@ -0,0 +1,28 @@
+<script setup>
+import { useMainStore } from "@/stores/store";
+import Footer from "../components/Footer.vue";
+
+const store = useMainStore();
+const apiUrl = import.meta.env.VITE_API_URL;
+const imgUrl = import.meta.env.VITE_API_IMG_URL;
+</script>
+
+<template>
+  <div class="content main-bg">
+    <v-container class="px-5 px-sm-15">
+      <img class="w-100" :src="store.imgPath" alt="" />
+    </v-container>
+    <Footer url="/step3" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.content {
+  padding: 0;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+</style>