SyuanYu 9 月之前
父節點
當前提交
d9976992bf

+ 5 - 0
app.d.ts

@@ -0,0 +1,5 @@
+declare module "*.vue" {
+    import { ComponentOptions } from "vue"
+    const componentOptions: ComponentOptions
+    export default componentOptions
+}

+ 1 - 1
index.html

@@ -3,7 +3,7 @@
 
 <head>
   <meta charset="UTF-8">
-  <link rel="icon" href="/favicon.ico">
+  <link rel="icon" href="/favicon.png">
   <!-- Google Fonts -->
   <link rel="preconnect" href="https://fonts.googleapis.com">
   <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

+ 1 - 0
jsconfig.json

@@ -4,5 +4,6 @@
       "@/*": ["./src/*"]
     }
   },
+  "include": ["app.d.ts"],
   "exclude": ["node_modules", "dist"]
 }

+ 197 - 2
package-lock.json

@@ -1,11 +1,11 @@
 {
-  "name": "ai-chatbot  ",
+  "name": "ai-chatbot",
   "version": "0.0.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
-      "name": "ai-chatbot  ",
+      "name": "ai-chatbot",
       "version": "0.0.0",
       "dependencies": {
         "@mdi/font": "^7.4.47",
@@ -13,6 +13,8 @@
         "axios": "^1.6.7",
         "pinia": "^2.1.7",
         "vue": "^3.3.11",
+        "vue-i18n": "^9.13.1",
+        "vue-picture-cropper": "^0.7.0",
         "vue-router": "^4.2.5"
       },
       "devDependencies": {
@@ -34,6 +36,14 @@
         "node": ">=6.0.0"
       }
     },
+    "node_modules/@bassist/utils": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/@bassist/utils/-/utils-0.4.0.tgz",
+      "integrity": "sha512-aoFTl0jUjm8/tDZodP41wnEkvB+C5O9NFCuYN/ztL6jSUSsuBkXq90/1ifBm1XhV/zySHgLYlU1+tgo3XtQ+nA==",
+      "dependencies": {
+        "@withtypes/mime": "^0.1.2"
+      }
+    },
     "node_modules/@esbuild/aix-ppc64": {
       "version": "0.19.12",
       "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
@@ -402,6 +412,47 @@
         "node": ">=12"
       }
     },
+    "node_modules/@intlify/core-base": {
+      "version": "9.13.1",
+      "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.13.1.tgz",
+      "integrity": "sha512-+bcQRkJO9pcX8d0gel9ZNfrzU22sZFSA0WVhfXrf5jdJOS24a+Bp8pozuS9sBI9Hk/tGz83pgKfmqcn/Ci7/8w==",
+      "dependencies": {
+        "@intlify/message-compiler": "9.13.1",
+        "@intlify/shared": "9.13.1"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      }
+    },
+    "node_modules/@intlify/message-compiler": {
+      "version": "9.13.1",
+      "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.13.1.tgz",
+      "integrity": "sha512-SKsVa4ajYGBVm7sHMXd5qX70O2XXjm55zdZB3VeMFCvQyvLew/dLvq3MqnaIsTMF1VkkOb9Ttr6tHcMlyPDL9w==",
+      "dependencies": {
+        "@intlify/shared": "9.13.1",
+        "source-map-js": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      }
+    },
+    "node_modules/@intlify/shared": {
+      "version": "9.13.1",
+      "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.13.1.tgz",
+      "integrity": "sha512-u3b6BKGhE6j/JeRU6C/RL2FgyJfy6LakbtfeVF8fJXURpZZTzfh3e05J0bu0XPw447Q6/WUp3C4ajv4TMS4YsQ==",
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      }
+    },
     "node_modules/@jridgewell/sourcemap-codec": {
       "version": "1.4.15",
       "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
@@ -708,6 +759,14 @@
         "vuetify": "^3.0.0"
       }
     },
+    "node_modules/@withtypes/mime": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/@withtypes/mime/-/mime-0.1.2.tgz",
+      "integrity": "sha512-PB9BfZGzwblUONJY0LiOwsHCA6uV3DIPj/w9ReekdHxPOl0VdUFgI5s4avKycuuq9Gf5Nz2ZPA2O36GAUzlMPA==",
+      "dependencies": {
+        "mime": "^3.0.0"
+      }
+    },
     "node_modules/animate.css": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",
@@ -800,6 +859,11 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/cropperjs": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.2.tgz",
+      "integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA=="
+    },
     "node_modules/csstype": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -1013,6 +1077,17 @@
         "node": ">=12"
       }
     },
+    "node_modules/mime": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+      "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
     "node_modules/mime-db": {
       "version": "1.52.0",
       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -1244,6 +1319,20 @@
         "node": ">=8.0"
       }
     },
+    "node_modules/typescript": {
+      "version": "4.9.5",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+      "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+      "optional": true,
+      "peer": true,
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=4.2.0"
+      }
+    },
     "node_modules/upath": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz",
@@ -1348,6 +1437,37 @@
         }
       }
     },
+    "node_modules/vue-i18n": {
+      "version": "9.13.1",
+      "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.13.1.tgz",
+      "integrity": "sha512-mh0GIxx0wPtPlcB1q4k277y0iKgo25xmDPWioVVYanjPufDBpvu5ySTjP5wOrSvlYQ2m1xI+CFhGdauv/61uQg==",
+      "dependencies": {
+        "@intlify/core-base": "9.13.1",
+        "@intlify/shared": "9.13.1",
+        "@vue/devtools-api": "^6.5.0"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/kazupon"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
+    "node_modules/vue-picture-cropper": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/vue-picture-cropper/-/vue-picture-cropper-0.7.0.tgz",
+      "integrity": "sha512-NF7+Dgso6d0GB16E5d/BbrcTIHm1VWz8dS3IjLhoBl+ZeC+yDA46CyJphQuO32SisaPmrKHN8VbiE2LgAfhnkQ==",
+      "dependencies": {
+        "@bassist/utils": "^0.4.0",
+        "cropperjs": "^1.6.1"
+      },
+      "peerDependencies": {
+        "vue": ">=3.2.13"
+      }
+    },
     "node_modules/vue-router": {
       "version": "4.2.5",
       "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.5.tgz",
@@ -1403,6 +1523,14 @@
       "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz",
       "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA=="
     },
+    "@bassist/utils": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/@bassist/utils/-/utils-0.4.0.tgz",
+      "integrity": "sha512-aoFTl0jUjm8/tDZodP41wnEkvB+C5O9NFCuYN/ztL6jSUSsuBkXq90/1ifBm1XhV/zySHgLYlU1+tgo3XtQ+nA==",
+      "requires": {
+        "@withtypes/mime": "^0.1.2"
+      }
+    },
     "@esbuild/aix-ppc64": {
       "version": "0.19.12",
       "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
@@ -1564,6 +1692,29 @@
       "dev": true,
       "optional": true
     },
+    "@intlify/core-base": {
+      "version": "9.13.1",
+      "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.13.1.tgz",
+      "integrity": "sha512-+bcQRkJO9pcX8d0gel9ZNfrzU22sZFSA0WVhfXrf5jdJOS24a+Bp8pozuS9sBI9Hk/tGz83pgKfmqcn/Ci7/8w==",
+      "requires": {
+        "@intlify/message-compiler": "9.13.1",
+        "@intlify/shared": "9.13.1"
+      }
+    },
+    "@intlify/message-compiler": {
+      "version": "9.13.1",
+      "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.13.1.tgz",
+      "integrity": "sha512-SKsVa4ajYGBVm7sHMXd5qX70O2XXjm55zdZB3VeMFCvQyvLew/dLvq3MqnaIsTMF1VkkOb9Ttr6tHcMlyPDL9w==",
+      "requires": {
+        "@intlify/shared": "9.13.1",
+        "source-map-js": "^1.0.2"
+      }
+    },
+    "@intlify/shared": {
+      "version": "9.13.1",
+      "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.13.1.tgz",
+      "integrity": "sha512-u3b6BKGhE6j/JeRU6C/RL2FgyJfy6LakbtfeVF8fJXURpZZTzfh3e05J0bu0XPw447Q6/WUp3C4ajv4TMS4YsQ=="
+    },
     "@jridgewell/sourcemap-codec": {
       "version": "1.4.15",
       "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
@@ -1779,6 +1930,14 @@
         "upath": "^2.0.1"
       }
     },
+    "@withtypes/mime": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/@withtypes/mime/-/mime-0.1.2.tgz",
+      "integrity": "sha512-PB9BfZGzwblUONJY0LiOwsHCA6uV3DIPj/w9ReekdHxPOl0VdUFgI5s4avKycuuq9Gf5Nz2ZPA2O36GAUzlMPA==",
+      "requires": {
+        "mime": "^3.0.0"
+      }
+    },
     "animate.css": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",
@@ -1848,6 +2007,11 @@
         "delayed-stream": "~1.0.0"
       }
     },
+    "cropperjs": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.2.tgz",
+      "integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA=="
+    },
     "csstype": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -1992,6 +2156,11 @@
         "@jridgewell/sourcemap-codec": "^1.4.15"
       }
     },
+    "mime": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+      "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="
+    },
     "mime-db": {
       "version": "1.52.0",
       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -2122,6 +2291,13 @@
         "is-number": "^7.0.0"
       }
     },
+    "typescript": {
+      "version": "4.9.5",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+      "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+      "optional": true,
+      "peer": true
+    },
     "upath": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz",
@@ -2163,6 +2339,25 @@
         "@vue/shared": "3.4.15"
       }
     },
+    "vue-i18n": {
+      "version": "9.13.1",
+      "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.13.1.tgz",
+      "integrity": "sha512-mh0GIxx0wPtPlcB1q4k277y0iKgo25xmDPWioVVYanjPufDBpvu5ySTjP5wOrSvlYQ2m1xI+CFhGdauv/61uQg==",
+      "requires": {
+        "@intlify/core-base": "9.13.1",
+        "@intlify/shared": "9.13.1",
+        "@vue/devtools-api": "^6.5.0"
+      }
+    },
+    "vue-picture-cropper": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/vue-picture-cropper/-/vue-picture-cropper-0.7.0.tgz",
+      "integrity": "sha512-NF7+Dgso6d0GB16E5d/BbrcTIHm1VWz8dS3IjLhoBl+ZeC+yDA46CyJphQuO32SisaPmrKHN8VbiE2LgAfhnkQ==",
+      "requires": {
+        "@bassist/utils": "^0.4.0",
+        "cropperjs": "^1.6.1"
+      }
+    },
     "vue-router": {
       "version": "4.2.5",
       "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.5.tgz",

+ 3 - 1
package.json

@@ -1,5 +1,5 @@
 {
-  "name": "ai-chatbot  ",
+  "name": "ai-chatbot",
   "version": "0.0.0",
   "private": true,
   "type": "module",
@@ -14,6 +14,8 @@
     "axios": "^1.6.7",
     "pinia": "^2.1.7",
     "vue": "^3.3.11",
+    "vue-i18n": "^9.13.1",
+    "vue-picture-cropper": "^0.7.0",
     "vue-router": "^4.2.5"
   },
   "devDependencies": {

二進制
public/favicon.ico


二進制
public/favicon.png


+ 1 - 1
src/App.vue

@@ -5,7 +5,7 @@ import { RouterLink, RouterView } from "vue-router";
 onMounted(() => {
   const url = window.location.href;
   // 動態切換 Title
-  if (url.includes("interact")) {
+  if (url.includes("skylantern")) {
     document.title = "AI 天燈";
   } else {
     document.title = "AI 明信片";

+ 5 - 2
src/components/Footer.vue

@@ -1,12 +1,15 @@
 <script setup>
+import { useI18n } from "vue-i18n";
+
+const { t, locale } = useI18n();
 const props = defineProps(["url", "back"]);
 </script>
 
 <template>
   <footer>
     <router-link :to="props.url">上一步</router-link>
-    <router-link :to="props.back ? props.back : '/'">回到首頁</router-link>
-    <!-- <router-link to="/">回到首頁</router-link> -->
+    <!-- <router-link :to="props.back ? props.back : '/'">回到首頁</router-link> -->
+    <a href="https://cmm.ai/101-ai-chatbot/#/">回到首頁</a>
   </footer>
 </template>
 

+ 15 - 2
src/components/Language.vue

@@ -1,11 +1,24 @@
 <script setup>
-import { reactive } from "vue";
-import { useRouter } from "vue-router";
+import { reactive, onMounted } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import { useI18n } from "vue-i18n";
 
+const route = useRoute();
 const router = useRouter();
+const { t, locale } = useI18n();
+
+onMounted(() => {
+  let lang = route.params.lang;
+  console.log("lang >>", lang);
+
+  if (lang && lang !== "") {
+    chooseLang(lang);
+  }
+});
 
 function chooseLang(lang) {
   console.log("選擇語言:", lang);
+  locale.value = lang; // i18n locale
   localStorage.setItem("lang", lang);
   router.push("/step1");
 }

+ 107 - 0
src/language/en.json

@@ -0,0 +1,107 @@
+{
+  "taipei_yangmingshan": "Taipei Yangmingshan Mother's Day",
+  "taipei_yangmingshan_description": "",
+
+  "tainan_confucius_temple": "Tainan Confucius Temple",
+  "tainan_confucius_temple_description": "The Confucius Temple in Tainan was built in 1665, it was the first Confucius Temple, before the end of Manchu Dynasty, it was the location of the highest official institute of higher learning in Taiwan. The Taiwan Palace of \"Confucianism Study\" it owned the incompatible position of culture and education, and was called \"The Highest Institute.\"",
+
+  "taipei_chiang_kai_shek_memorial_hall": "Taipei Chiang Kai-Shek Memorial Hall",
+  "taipei_chiang_kai_shek_memorial_hall_description": "The designer of the Chiang Kai-shek Memorial Hall was architect Yang Cho-cheng, who also designed the Grand Hotel. The hall was built upon what had been an army headquarters that was part of the largest military district in Taipei City. The blue and white colors used in the memorial hall are key colors in the national flag. The blue and white of the top of the memorial represent the blue sky and 12 rays of a white sun. With Taiwan's evolution toward democracy and openness, the square in front of the memorial hall has become a popular gathering place and part of this social trend.",
+
+  "taipei_chiang_kai_shek_memorial_hall_2": "Taipei Chiang Kai-Shek Memorial Hall-2",
+  "taipei_chiang_kai_shek_memorial_hall_2_description": "The designer of the Chiang Kai-shek Memorial Hall was architect Yang Cho-cheng, who also designed the Grand Hotel. The hall was built upon what had been an army headquarters that was part of the largest military district in Taipei City. The blue and white colors used in the memorial hall are key colors in the national flag. The blue and white of the top of the memorial represent the blue sky and 12 rays of a white sun. With Taiwan's evolution toward democracy and openness, the square in front of the memorial hall has become a popular gathering place and part of this social trend.",
+
+  "taitung_jiaming_lake": "Taitung Jiaming Lake",
+  "taitung_jiaming_lake_description": "The Jiaminghu National Trail is in the South Second Section of the Central Mountain Range. The trail is clear to see and the high-mountain ecology is rich and changeful. It runs through a Taiwan hemlock forest and a fir forest, high mountains and deep valleys, broken cliffs and collapsed mountain walls and a arrow bamboo grassland and along the ridgeline. The deer in the valleys, one after another mountain peak, snow in winter, silver moon and sky full of stars have long been beautiful sights that attract hikers. The ecology and landscape on Xiangyang Mountain and Sancha Mountain the trail passes by have their own characteristics. Xiangyang Mountain is the southernmost border of the spread of high-mountain ecology in the Central Mountains, and Sancha Mountain is famous for the vast and magnificent high-mountain grassland and the sapphire-like Jiaming Lake. Meanwhile, the bright and colorful blossoms in spring and summer and the snow in winter on the two mountains are beautiful views not to be missed.",
+
+  "mid_autumn_festival": "Mid Autumn Festival",
+  "mid_autumn_festival_description": "",
+
+  "new_taipei_yehliu_queen_head": "New Taipei Yehliu Queen Head",
+  "new_taipei_yehliu_queen_head_description": "Yehliu Geo-park features all sorts of strange-looking rocks. It is part of Datun Mountain that stretches out to the sea. Weathering, marine erosion, and crustal movement together created the world-renowned scenery of mushroom rocks, sea caves, tofu rocks, candle rocks, and giant's kettles. Among all, Queen's Head is the most representative landmark and check-in attraction in Yehliu. Cute Princess, a rock that resembles a girl with a pony tail, is considered the successor of Queen's Head.",
+
+  "keelung_heping_island_park": "Keelung Heping Island Park",
+  "keelung_heping_island_park_description": "At the east of Keelung Port, Heping Island Park is characterised by special geological formations resulting from wind and sea erosions. The most famous are \"A Thousand Layers\" and \"Tend Thousand Piles\" (mushroom rocks). Other than these beautiful formations, the body fossils and trace fossils are the island's geological treasures. To protect these precious resources, the fossil zone is accessible to visitors only at certain hours in the form of guided tours.",
+
+  "taichung_sun_moon_lake": "Taichung Sun Moon Lake",
+  "taichung_sun_moon_lake_description": "Sun Moon Lake is a beautiful mountain lake located in Yuchi Township, Nantou County, Taiwan. The lake is bounded by Lalu Island. The east side is shaped like a \"sun\" and the west side is shaped like a \"moon\", hence the name Sun Moon Lake. It is surrounded by mountains and rivers, lush forests, and the pool is as broad and level as a mirror, reflecting the ever-changing mountains.",
+
+  "tainan_salt_field": "Tainan Salt Field",
+  "tainan_salt_field_description": "",
+
+  "kaohsiung_formosa_boulevard_station": "Kaohsiung Formosa Boulevard Station",
+  "kaohsiung_formosa_boulevard_station_description": "",
+
+  "new_taipei_shifen_waterfall": "New Taipei Shifen Waterfall",
+  "new_taipei_shifen_waterfall_description": "",
+
+  "kaohsiung_music_center": "Kaohsiung Music Center",
+  "kaohsiung_music_center_description": "",
+
+  "national_taichung_theater": "National Taichung Theater",
+  "national_taichung_theater_description": "",
+
+  "chiayi_song_of_forest": "Chiayi Song Of Forest",
+  "chiayi_song_of_forest_description": "",
+
+  "keelung_baduzi_railway": "Keelung Baduzi Railway",
+  "keelung_baduzi_railway_description": "",
+
+  "hualien_qingshui_cliff": "Hualien Qingshui Cliff",
+  "hualien_qingshui_cliff_description": "",
+
+  "taipei_national_palace_museum": "Taipei National Palace Museum",
+  "taipei_national_palace_museum_description": "The Taipei National Palace Museum is a world-class museum that hosts an eclectic collection of treasures kept by generations of Emperors ruling from the Forbidden City. In WWII, Nationalist troops seized the most important pieces in order to prevent invaders from ransacking China's national treasures. A twist of fate eventually brought these treasures to Taiwan.",
+
+  "taipei_national_palace_museum_2": "Taipei National Palace Museum-2",
+  "taipei_national_palace_museum_2_description": "The Taipei National Palace Museum is a world-class museum that hosts an eclectic collection of treasures kept by generations of Emperors ruling from the Forbidden City. In WWII, Nationalist troops seized the most important pieces in order to prevent invaders from ransacking China's national treasures. A twist of fate eventually brought these treasures to Taiwan.",
+
+  "penghu": "Penghu",
+  "penghu_description": "",
+
+  "nantou_qingjing_farm": "Nantou Qingjing Farm",
+  "nantou_qingjing_farm_description": "",
+
+  "hualien_daylily_mountain": "Hualien Daylily Mountain",
+  "hualien_daylily_mountain_description": "",
+
+  "new_taipei_jiufen_old_street": "New Taipei Jiufen Old Street",
+  "new_taipei_jiufen_old_street_description": "",
+
+  "chiayi_alishan_forest_railways": "Chiayi Alishan Forest Railways",
+  "chiayi_alishan_forest_railways_description": "",
+
+  "taichung_gaomei_wetland": "Taichung Gaomei Wetland",
+  "taichung_gaomei_wetland_description": "",
+
+  "skylantern": {
+    "home": {
+      "text_1": "在這裡",
+      "text_2": "選一顆天燈",
+      "text_3": "寫下你的祝福或願望吧",
+      "text_4": "您傳遞出去的願望",
+      "text_5": "會在 101 觀景台 5F 購票處的",
+      "text_6": "環景螢幕牆旁的螢幕定時播放"
+    },
+    "step1": {
+      "text_1": "天燈的起源",
+      "text_2": "「天燈」起初是為了傳遞訊息之用,但目前通常則被當成節慶折福許願的工具,象徵收穫的成功和幸福每一年。許多旅遊景點每天晚上都提供放天燈的活動",
+      "text_3": "祈福目的",
+      "text_4": "健康|平安",
+      "text_5": "幸福|快樂",
+      "text_6": "前途|事業",
+      "text_7": "愛情|婚姻",
+      "text_8": "金錢|發財",
+      "text_9": "考試|讀書",
+      "text_10": "環保天燈",
+      "text_11": "天燈是鐵絲或竹子,紙、油紙組成。當天燈的燃料燒完後,會掉落附近地面,餘火會燒到附近住家的屋頂或菜園、甚至引發森林大火,並且材料會殘留重金屬,可能對生態鏈造成嚴重危害。於是2018年起台灣研發出了能在空中徹底燃燒成灰的",
+      "text_12": "[全紙環保天燈]",
+      "text_13": "當然,在101放天燈,也非常環保"
+    },
+    "wish": "寫下你的願望",
+    "preview": "預覽",
+    "submit": "送出",
+    "success": "送出成功",
+    "received": "已經收到您的天燈"
+  }
+}

+ 107 - 0
src/language/ja.json

@@ -0,0 +1,107 @@
+{
+  "taipei_yangmingshan": "臺北陽明山-母親節",
+  "taipei_yangmingshan_description": "",
+
+  "tainan_confucius_temple": "台南孔廟",
+  "tainan_confucius_temple_description": "1665年に創建された台南の孔子廟は国内初の孔子廟として知られます。清朝末期までは台湾政府の最高学府とされ、台湾の「儒学」の中心だったところです。教育分野ではこれに匹敵するものはなく、「全台首学」と呼ばれていました。",
+
+  "taipei_chiang_kai_shek_memorial_hall": "臺北中正紀念堂",
+  "taipei_chiang_kai_shek_memorial_hall_description": "中正紀念堂の設計者は圓山大飯店を設計した楊卓成氏です。紀念堂は元々は陸軍総司令部があった場所で、ここは当時台北市内で最大の軍事エリアでした。紀念堂に用いられている青と白の二色は中華民国旗のメインカラーです。紀念堂上部の空の装飾は青天白日の12本の光線を表しています。民主化の発展に伴い、思想が開放され、紀念堂前の広場は民主運動の集会場となりました。",
+
+  "taipei_chiang_kai_shek_memorial_hall_2": "臺北中正紀念堂-2",
+  "taipei_chiang_kai_shek_memorial_hall_2_description": "中正紀念堂の設計者は圓山大飯店を設計した楊卓成氏です。紀念堂は元々は陸軍総司令部があった場所で、ここは当時台北市内で最大の軍事エリアでした。紀念堂に用いられている青と白の二色は中華民国旗のメインカラーです。紀念堂上部の空の装飾は青天白日の12本の光線を表しています。民主化の発展に伴い、思想が開放され、紀念堂前の広場は民主運動の集会場となりました。",
+
+  "taitung_jiaming_lake": "台東嘉明湖",
+  "taitung_jiaming_lake_description": "嘉明湖国家公園の步道は中央山脈南二段の一部分であり、全部の步道は良く見えられ、豊かで変化が多い高山の生態景観があって、タイワンツガの森、台湾モミの森、高山深谷、断崖絶壁、ヤダケ草原及び山頂の稜線は皆非常に壮麗である。谷から伝える鹿の鳴き声、群山が連なり、冬には雪が降り、綺麗な月光と満天の星々などは昔から登山者を惹きつける麗しい景色である。步道が経由する向陽山とサンサ(三叉)山の生態景観はそれぞれの特色があり、向陽山は中央山脈高山のツンドラ生態が分布する最南端の限界である。サンサ(三叉)山は広くて開放的壮麗な高山草原及びサファイアみたいな嘉明湖が有名であり、この二つの山の春夏の季節は百花繚乱に山野の花々が満開し、冬の日の雪景色は、絶対見る価値がある美景。",
+
+  "mid_autumn_festival": "中秋節",
+  "mid_autumn_festival_description": "",
+
+  "new_taipei_yehliu_queen_head": "新北野柳女王頭",
+  "new_taipei_yehliu_queen_head_description": "奇岩や美しい石が見られる野柳地質公園は、大屯山の支脈が海の中に入り込んだ岬で、風化、海食、地殻運動などの影響により、蕈状岩(キノコ岩)、海食洞、豆腐石、燭状岩、甌穴など、国際的にも有名な自然の奇景が形成されています。中でも、野柳を代表するシンボル、「女王頭(クイーンズヘッド)」はSNSの人気チェックインスポットです。ポニーテールを結ったような「俏皮公主(おてんばなお姫様)」は、女王頭の後継者として知られています。",
+
+  "keelung_heping_island_park": "基隆和平島公園",
+  "keelung_heping_island_park_description": "和平島地質公園は、基隆港の東側に位置し、長い間、風と波によって浸食され続けたため、特殊な地形になっています。なかでも有名なのは「千畳敷」と「萬人堆(キノコ石)」です。こうした美しい地形のほか、地層にむき出しになった生物本体の化石と生痕化石が、和平島の大切な宝です。貴重な地質資源を守るため、岩石管制区においては、予約制かつ定時制のガイド方式をとっています。",
+
+  "taichung_sun_moon_lake": "台中日月潭",
+  "taichung_sun_moon_lake_description": "日月潭は、台湾の南投県​​魚池郷にある美しい山間の湖です。ラル島に囲まれた湖は、東側が「太陽」、西側が「月」の形をしていることから、日月潭と呼ばれています。山と川、緑豊かな森林に囲まれ、鏡のように広く水平なプールは、刻々と変化する山々を映し出します。",
+
+  "tainan_salt_field": "台南鹽田",
+  "tainan_salt_field_description": "",
+
+  "kaohsiung_formosa_boulevard_station": "高雄美麗島",
+  "kaohsiung_formosa_boulevard_station_description": "",
+
+  "new_taipei_shifen_waterfall": "新北十分瀑布",
+  "new_taipei_shifen_waterfall_description": "",
+
+  "kaohsiung_music_center": "高雄流行音樂中心",
+  "kaohsiung_music_center_description": "",
+
+  "national_taichung_theater": "台中歌劇院",
+  "national_taichung_theater_description": "",
+
+  "chiayi_song_of_forest": "嘉義森林之歌",
+  "chiayi_song_of_forest_description": "",
+
+  "keelung_baduzi_railway": "基隆八斗子鐵路",
+  "keelung_baduzi_railway_description": "",
+
+  "hualien_qingshui_cliff": "花蓮清水斷崖",
+  "hualien_qingshui_cliff_description": "",
+
+  "taipei_national_palace_museum": "臺北故宮",
+  "taipei_national_palace_museum_description": "台北にある「国立故宮博物院」は、世界屈指の大型博物館です。なお、所蔵品の多くは北京の故宮歴代皇帝が集めたもので、第2次世界大戦の際敵方の侵略を免れるため、国軍の手によって宮廷内の所蔵品が精選され台湾へ運び出されました。",
+
+  "taipei_national_palace_museum_2": "臺北故宮-2",
+  "taipei_national_palace_museum_2_description": "台北にある「国立故宮博物院」は、世界屈指の大型博物館です。なお、所蔵品の多くは北京の故宮歴代皇帝が集めたもので、第2次世界大戦の際敵方の侵略を免れるため、国軍の手によって宮廷内の所蔵品が精選され台湾へ運び出されました。",
+
+  "penghu": "澎湖",
+  "penghu_description": "",
+
+  "nantou_qingjing_farm": "南投清境農場",
+  "nantou_qingjing_farm_description": "",
+
+  "hualien_daylily_mountain": "花蓮金針花山",
+  "hualien_daylily_mountain_description": "",
+
+  "new_taipei_jiufen_old_street": "新北九份老街",
+  "new_taipei_jiufen_old_street_description": "",
+
+  "chiayi_alishan_forest_railways": "嘉義阿里山小火車",
+  "chiayi_alishan_forest_railways_description": "",
+
+  "taichung_gaomei_wetland": "台中高美濕地",
+  "taichung_gaomei_wetland_description": "",
+
+  "skylantern": {
+    "home": {
+      "text_1": "こちらで",
+      "text_2": "ランタン(天燈)を選んで、",
+      "text_3": "祈りか願い事を書きましょう。",
+      "text_4": "書いた願い事は",
+      "text_5": "台北101の展望台5階にあるチケット売り場の",
+      "text_6": "液晶ディスプレイに放映されます。"
+    },
+    "step1": {
+      "text_1": "ランタンの由来",
+      "text_2": "「ランタン」は昔に通信手段として使用されましたが、現代ならお祭りに豊作と幸せの一年になりますよう、祈りや願い事をするツールとして使われています。台湾の観光スポットに毎晩ランタンを飛ばすイベントを行っています。",
+      "text_3": "何を願う",
+      "text_4": "健康祈願|家内安全",
+      "text_5": "幸福|喜ぶ",
+      "text_6": "出世成功|商売繫盛",
+      "text_7": "恋愛成就|夫婦円満",
+      "text_8": "千客万来|財産上昇",
+      "text_9": "学業成就|合格祈願",
+      "text_10": "スカイランタン",
+      "text_11": "スカイランタンはワイヤー、竹、水の耐性に強い紙、油紙でできています。スカイランタンの燃料が燃え尽きたら地面に落ち、残りの火は近くの家の屋根や菜園を燃やしたり、森林火災を引き起こしたりする可能性があります。また、材料の中に重金属が含まれており、環境に大きな影響を及びます。そのため、台湾は2018年に空で一定の高さに達すると、スカイランタンは完全に燃え尽きるタイプの",
+      "text_12": "[紙スカイランタン]を開発しました。",
+      "text_13": "もちろん、台北101でランタンを飛ばすのも環境に優しいです。"
+    },
+    "wish": "願い事を書いてください。",
+    "preview": "プレビュー",
+    "submit": "送信",
+    "success": "送信完了!",
+    "received": "書いたランタンを届きました。"
+  }
+}

+ 107 - 0
src/language/ko.json

@@ -0,0 +1,107 @@
+{
+  "taipei_yangmingshan": "臺北陽明山-母親節",
+  "taipei_yangmingshan_description": "",
+
+  "tainan_confucius_temple": "台南孔廟",
+  "tainan_confucius_temple_description": "삼백년이 넘는 역사를 지닌 ‘전대수학(全台首學. 전국 최고의 학교)’ 공자묘는 타이난이 문화의 고도(古都)라 불리는 핵심이라 할 수 있습니다.",
+
+  "taipei_chiang_kai_shek_memorial_hall": "臺北中正紀念堂",
+  "taipei_chiang_kai_shek_memorial_hall_description": "장개석 기념관의 설계자는 그랜드 호텔을 설계한 건축가 양초청이었습니다. 이 홀은 타이베이시에서 가장 큰 군사 구역의 일부였던 육군 본부 자리에 지어졌습니다. 기념관에 사용되는 파란색과 흰색 색상은 국기의 주요 색상입니다. 기념비 꼭대기의 파란색과 흰색은 푸른 하늘과 하얀 태양의 12광선을 상징합니다. 대만의 민주화와 개방화로 인해 기념관 앞 광장은 인기 있는 모임 장소이자 이러한 사회적 추세의 일부가 되었습니다.",
+
+  "taipei_chiang_kai_shek_memorial_hall_2": "臺北中正紀念堂-2",
+  "taipei_chiang_kai_shek_memorial_hall_2_description": "장개석 기념관의 설계자는 그랜드 호텔을 설계한 건축가 양초청이었습니다. 이 홀은 타이베이시에서 가장 큰 군사 구역의 일부였던 육군 본부 자리에 지어졌습니다. 기념관에 사용되는 파란색과 흰색 색상은 국기의 주요 색상입니다. 기념비 꼭대기의 파란색과 흰색은 푸른 하늘과 하얀 태양의 12광선을 상징합니다. 대만의 민주화와 개방화로 인해 기념관 앞 광장은 인기 있는 모임 장소이자 이러한 사회적 추세의 일부가 되었습니다.",
+
+  "taitung_jiaming_lake": "台東嘉明湖",
+  "taitung_jiaming_lake_description": "자밍후(嘉明湖)국가보도는 중앙산맥 남2단의 일부입니다. 보도 코스로선이 뚜렷하고, 대만 삼나무림, 대만 솔농나무림, 고산심곡, 절벽, 초원 및 산정능선등 고산생태경관이 다양하고 풍부합니다. 동물의 소리, 연결된 군봉, 겨울의 설경, 흰 달빛과 별이 빛나는 야밤등이 오래전부터 등산객들의 핫한 코스이었습니다. 보도길은 샹양산(向陽山)과 산차산(三叉山) 생태경관을 지나고, 샹양산은 중앙산맥은 고산생태 분포의 가장 남쪽의 경계며, 산차산은 넓고 웅장한 고산초원 및 사파이어처럼 빛나는 자밍후(嘉明湖)로 득명했으며, 두 산의 봄 여름철의 피는 꽃과 겨울의 설경등 절대 놓칠 수 없는 미경입니다.",
+
+  "mid_autumn_festival": "中秋節",
+  "mid_autumn_festival_description": "",
+
+  "new_taipei_yehliu_queen_head": "新北野柳女王頭",
+  "new_taipei_yehliu_queen_head_description": "특이하고 아름다운 암석들을 보유하고 있는 예류지질공원은 다툰산의 여맥(餘脈)이 바다로 뻗어져 나와 형성한 곶입니다. 바람과 파도의 침식 그리고 지각 운동 등으로 버섯 바위, 해식동, 두부 바위, 촛불 바위와 돌개구멍 등 국제적으로 명성이 자자한 독특한 자연 경관을 자랑합니다. 이중 ‘여왕 머리 바위’는 예류를 대표하는 랜드마크이자 SNS 인기 명소이며, ‘귀여운 공주 바위’는 포니테일을 한 듯한 모양으로 여왕 머리 바위의 뒤를 이을 대표 랜드마크로 추앙받고 있습니다.",
+
+  "keelung_heping_island_park": "基隆和平島公園",
+  "keelung_heping_island_park_description": "지룽항 동쪽에 위치한 허핑다오 공원은 바람과 바다의 침식으로 인해 형성된 특별한 지질 구조가 특징입니다. 가장 유명한 것은 \"A Thousand Layers\"와 \"Tend Thousand Piles\"(버섯 바위)입니다. 이러한 아름다운 지형 외에도 신체 화석과 흔적 화석이 섬의 지질 보물입니다. 이러한 귀중한 자원을 보호하기 위해 화석지대는 가이드 투어 형태로 특정 시간에만 방문객이 접근할 수 있습니다.",
+
+  "taichung_sun_moon_lake": "台中日月潭",
+  "taichung_sun_moon_lake_description": "르웨탄 호수는 대만 난터우현 유치향에 위치한 아름다운 산악 호수입니다. 호수는 라루섬(Lalu Island)과 경계를 이루고 있으며 동쪽은 '해' 모양이고 서쪽은 '달' 모양이므로 일월담이라는 이름이 붙었습니다. 산과 강, 울창한 숲으로 둘러싸여 있으며 수영장은 거울처럼 넓고 평평하여 시시각각 변화하는 산을 반사합니다.",
+
+  "tainan_salt_field": "台南鹽田",
+  "tainan_salt_field_description": "",
+
+  "kaohsiung_formosa_boulevard_station": "高雄美麗島",
+  "kaohsiung_formosa_boulevard_station_description": "",
+
+  "new_taipei_shifen_waterfall": "新北十分瀑布",
+  "new_taipei_shifen_waterfall_description": "",
+
+  "kaohsiung_music_center": "高雄流行音樂中心",
+  "kaohsiung_music_center_description": "",
+
+  "national_taichung_theater": "台中歌劇院",
+  "national_taichung_theater_description": "",
+
+  "chiayi_song_of_forest": "嘉義森林之歌",
+  "chiayi_song_of_forest_description": "",
+
+  "keelung_baduzi_railway": "基隆八斗子鐵路",
+  "keelung_baduzi_railway_description": "",
+
+  "hualien_qingshui_cliff": "花蓮清水斷崖",
+  "hualien_qingshui_cliff_description": "",
+
+  "taipei_national_palace_museum": "臺北故宮",
+  "taipei_national_palace_museum_description": "“국립 고궁박물관”은 전 세계에서 손 꼽히는 대형 박물관으로 2차대전시 국경의 고궁박물관의 문물이 파괴되는 것을 피하기 위해 중요한 수장품을 대만으로 옮겨온 곳이며, 이들 문물은 당, 송, 원, 명, 청 5대에 걸친 서화, 동기, 자기, 칠기, 조각, 선본서적, 문헌등 수십만건에 이르고 매우 높은 예술적 가치와 역사문화적 가치를 가지고 있다. 전람구역에는 중국어, 영어, 프랑스어, 독일어, 일본어, 한국어 등 7개 언어로 전문적인 안내를 하고 있다.",
+
+  "taipei_national_palace_museum_2": "臺北故宮-2",
+  "taipei_national_palace_museum_2_description": "“국립 고궁박물관”은 전 세계에서 손 꼽히는 대형 박물관으로 2차대전시 국경의 고궁박물관의 문물이 파괴되는 것을 피하기 위해 중요한 수장품을 대만으로 옮겨온 곳이며, 이들 문물은 당, 송, 원, 명, 청 5대에 걸친 서화, 동기, 자기, 칠기, 조각, 선본서적, 문헌등 수십만건에 이르고 매우 높은 예술적 가치와 역사문화적 가치를 가지고 있다. 전람구역에는 중국어, 영어, 프랑스어, 독일어, 일본어, 한국어 등 7개 언어로 전문적인 안내를 하고 있다.",
+
+  "penghu": "澎湖",
+  "penghu_description": "",
+
+  "nantou_qingjing_farm": "南投清境農場",
+  "nantou_qingjing_farm_description": "",
+
+  "hualien_daylily_mountain": "花蓮金針花山",
+  "hualien_daylily_mountain_description": "",
+
+  "new_taipei_jiufen_old_street": "新北九份老街",
+  "new_taipei_jiufen_old_street_description": "",
+
+  "chiayi_alishan_forest_railways": "嘉義阿里山小火車",
+  "chiayi_alishan_forest_railways_description": "",
+
+  "taichung_gaomei_wetland": "台中高美濕地",
+  "taichung_gaomei_wetland_description": "",
+
+  "skylantern": {
+    "home": {
+      "text_1": "在這裡",
+      "text_2": "選一顆天燈",
+      "text_3": "寫下你的祝福或願望吧",
+      "text_4": "您傳遞出去的願望",
+      "text_5": "會在 101 觀景台 5F 購票處的",
+      "text_6": "環景螢幕牆旁的螢幕定時播放"
+    },
+    "step1": {
+      "text_1": "天燈的起源",
+      "text_2": "「天燈」起初是為了傳遞訊息之用,但目前通常則被當成節慶折福許願的工具,象徵收穫的成功和幸福每一年。許多旅遊景點每天晚上都提供放天燈的活動",
+      "text_3": "祈福目的",
+      "text_4": "健康|平安",
+      "text_5": "幸福|快樂",
+      "text_6": "前途|事業",
+      "text_7": "愛情|婚姻",
+      "text_8": "金錢|發財",
+      "text_9": "考試|讀書",
+      "text_10": "環保天燈",
+      "text_11": "天燈是鐵絲或竹子,紙、油紙組成。當天燈的燃料燒完後,會掉落附近地面,餘火會燒到附近住家的屋頂或菜園、甚至引發森林大火,並且材料會殘留重金屬,可能對生態鏈造成嚴重危害。於是2018年起台灣研發出了能在空中徹底燃燒成灰的",
+      "text_12": "[全紙環保天燈]",
+      "text_13": "當然,在101放天燈,也非常環保"
+    },
+    "wish": "寫下你的願望",
+    "preview": "預覽",
+    "submit": "送出",
+    "success": "送出成功",
+    "received": "已經收到您的天燈"
+  }
+}

+ 107 - 0
src/language/zh.json

@@ -0,0 +1,107 @@
+{
+  "taipei_yangmingshan": "臺北陽明山-母親節",
+  "taipei_yangmingshan_description": "",
+
+  "tainan_confucius_temple": "台南孔廟",
+  "tainan_confucius_temple_description": "「全臺首學」臺灣的第一座孔子廟,臺南孔子廟創建於明永曆19年(1665年),當時稱為「先師聖廟」,至今已有三百多年的歷史,由島上第一個漢人政權鄭氏王朝所創立,為的是在臺開辦教育,培養為國效命的人才。清領時期亦延續功能,為臺灣官辦的最高學府「臺灣府學」所在地。直到今日,孔廟依然是讀書人的聖廟,有著崇高的地位。",
+
+  "taipei_chiang_kai_shek_memorial_hall": "臺北中正紀念堂",
+  "taipei_chiang_kai_shek_memorial_hall_description": "中正紀念堂的設計師是設計圓山大飯店的楊卓成,紀念堂原址是陸軍總部,當時是臺北市區內最大的軍區,紀念堂採用的藍白兩色,是國旗上面主要的顏色,紀念堂頂部天穹的裝飾是青天白日12道光芒。隨著民主演進思想開放,紀念堂前的廣場已成了民主運動的集會場。",
+
+  "taipei_chiang_kai_shek_memorial_hall_2": "臺北中正紀念堂-2",
+  "taipei_chiang_kai_shek_memorial_hall_2_description": "中正紀念堂的設計師是設計圓山大飯店的楊卓成,紀念堂原址是陸軍總部,當時是臺北市區內最大的軍區,紀念堂採用的藍白兩色,是國旗上面主要的顏色,紀念堂頂部天穹的裝飾是青天白日12道光芒。隨著民主演進思想開放,紀念堂前的廣場已成了民主運動的集會場。",
+
+  "taitung_jiaming_lake": "台東嘉明湖",
+  "taitung_jiaming_lake_description": "嘉明湖國家步道為中央山脈南二段的一部分,步道全程路徑明顯,高山生態景觀豐富多變,穿越台灣鐵杉林、台灣冷杉林、高山深谷、斷崖崩壁、箭竹草原及山頂稜線;而空谷鹿鳴、相連群峰、冬日雪景、皎潔月色和滿天星子是長久以來吸引登山客的美景。",
+
+  "mid_autumn_festival": "中秋節",
+  "mid_autumn_festival_description": "",
+
+  "new_taipei_yehliu_queen_head": "新北野柳女王頭",
+  "new_taipei_yehliu_queen_head_description": "擁有奇岩美石的野柳地質公園,是大屯山餘脈延伸至海中的岬角,受到風化、海蝕及地殼運動等作用,造就了蕈狀岩、海蝕洞、豆腐石、燭狀岩及壺穴等奇景,是揚名國際的天然風景名勝地。其中,「女王頭」更是野柳具代表性的地標與熱門打卡點;「俏皮公主」則有著如綁著馬尾般的造型,被譽為女王頭的接班人。",
+
+  "keelung_heping_island_park": "基隆和平島公園",
+  "keelung_heping_island_park_description": "和平島地質公園位於基隆港港口東側,是基隆港的門戶,同時也是北台灣最早有西方人足跡的地方,亦是基隆最早有漢人入墾的所在地之一。此地早期原為凱達格蘭族的聚落,之後被列為軍事管制區,目前在沿海部份地區已開放設為和平島地質公園。",
+
+  "taichung_sun_moon_lake": "台中日月潭",
+  "taichung_sun_moon_lake_description": "日月潭位於臺灣南投縣魚池鄉,是一座美麗的高山湖泊。潭面以拉魯島為界,東側形如「日」,西側形如「月」,故名日月潭。其周圍山重水複,林木蒼鬱,潭面廣闊水平如鏡,映照群山變化萬千。",
+
+  "tainan_salt_field": "台南鹽田",
+  "tainan_salt_field_description": "",
+
+  "kaohsiung_formosa_boulevard_station": "高雄美麗島",
+  "kaohsiung_formosa_boulevard_station_description": "",
+
+  "new_taipei_shifen_waterfall": "新北十分瀑布",
+  "new_taipei_shifen_waterfall_description": "",
+
+  "kaohsiung_music_center": "高雄流行音樂中心",
+  "kaohsiung_music_center_description": "",
+
+  "national_taichung_theater": "台中歌劇院",
+  "national_taichung_theater_description": "",
+
+  "chiayi_song_of_forest": "嘉義森林之歌",
+  "chiayi_song_of_forest_description": "",
+
+  "keelung_baduzi_railway": "基隆八斗子鐵路",
+  "keelung_baduzi_railway_description": "",
+
+  "hualien_qingshui_cliff": "花蓮清水斷崖",
+  "hualien_qingshui_cliff_description": "",
+
+  "taipei_national_palace_museum": "臺北故宮",
+  "taipei_national_palace_museum_description": "國立故宮博物院於1965年在外雙溪落成,中國宮殿式的建築,一至三樓為展覽陳列空間,四樓為休憩茶座「三希堂」。藏有全世界最多的中華藝術寶藏,收藏品主要承襲自宋、元、明、清四朝,幾乎涵蓋了整部五千年的中國歷史,數量近70萬件,使國立故宮博物院擁有「中華文化寶庫」的美名。",
+
+  "taipei_national_palace_museum_2": "臺北故宮-2",
+  "taipei_national_palace_museum_2_description": "國立故宮博物院於1965年在外雙溪落成,中國宮殿式的建築,一至三樓為展覽陳列空間,四樓為休憩茶座「三希堂」。藏有全世界最多的中華藝術寶藏,收藏品主要承襲自宋、元、明、清四朝,幾乎涵蓋了整部五千年的中國歷史,數量近70萬件,使國立故宮博物院擁有「中華文化寶庫」的美名。",
+
+  "penghu": "澎湖",
+  "penghu_description": "",
+
+  "nantou_qingjing_farm": "南投清境農場",
+  "nantou_qingjing_farm_description": "",
+
+  "hualien_daylily_mountain": "花蓮金針花山",
+  "hualien_daylily_mountain_description": "",
+
+  "new_taipei_jiufen_old_street": "新北九份老街",
+  "new_taipei_jiufen_old_street_description": "",
+
+  "chiayi_alishan_forest_railways": "嘉義阿里山小火車",
+  "chiayi_alishan_forest_railways_description": "",
+
+  "taichung_gaomei_wetland": "台中高美濕地",
+  "taichung_gaomei_wetland_description": "",
+
+  "skylantern": {
+    "home": {
+      "text_1": "在這裡",
+      "text_2": "選一顆天燈",
+      "text_3": "寫下你的祝福或願望吧",
+      "text_4": "您傳遞出去的願望",
+      "text_5": "會在 101 觀景台 5F 購票處的",
+      "text_6": "環景螢幕牆旁的螢幕定時播放"
+    },
+    "step1": {
+      "text_1": "天燈的起源",
+      "text_2": "「天燈」起初是為了傳遞訊息之用,但目前通常則被當成節慶折福許願的工具,象徵收穫的成功和幸福每一年。許多旅遊景點每天晚上都提供放天燈的活動",
+      "text_3": "祈福目的",
+      "text_4": "健康|平安",
+      "text_5": "幸福|快樂",
+      "text_6": "前途|事業",
+      "text_7": "愛情|婚姻",
+      "text_8": "金錢|發財",
+      "text_9": "考試|讀書",
+      "text_10": "環保天燈",
+      "text_11": "天燈是鐵絲或竹子,紙、油紙組成。當天燈的燃料燒完後,會掉落附近地面,餘火會燒到附近住家的屋頂或菜園、甚至引發森林大火,並且材料會殘留重金屬,可能對生態鏈造成嚴重危害。於是2018年起台灣研發出了能在空中徹底燃燒成灰的",
+      "text_12": "[全紙環保天燈]",
+      "text_13": "當然,在101放天燈,也非常環保"
+    },
+    "wish": "寫下你的願望",
+    "preview": "預覽",
+    "submit": "送出",
+    "success": "送出成功",
+    "received": "已經收到您的天燈"
+  }
+}

+ 2 - 0
src/main.js

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

+ 20 - 0
src/plugins/i18n.js

@@ -0,0 +1,20 @@
+import { createI18n } from 'vue-i18n';
+import zh from "../language/zh.json";
+import en from "../language/en.json";
+import ja from "../language/ja.json";
+import ko from "../language/ko.json";
+
+const i18n = createI18n({
+  legacy: false,
+  locale: localStorage.getItem("lang") ?? "zh",
+  fallbackLocale: "zh",
+  globalInjection: true,
+  messages: {
+    "zh": zh,
+    "en": en,
+    "ja": ja,
+    "ko": ko,
+  }
+})
+
+export default i18n

+ 47 - 22
src/router/index.js

@@ -1,3 +1,4 @@
+import { useMainStore } from "@/stores/store";
 import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
 import HomeView from '../views/HomeView.vue'
 import Language from "../components/Language.vue";
@@ -6,23 +7,24 @@ 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 Step_6 from "../views/Step_6.vue";
 // 天燈
-import Interact from '../views/Interact.vue'
-import InteractHome from '../views/InteractHome.vue'
-import InteractStep_1 from '../views/InteractStep_1.vue'
-import InteractStep_2 from '../views/InteractStep_2.vue'
-import InteractStep_3 from '../views/InteractStep_3.vue'
+import Skylantern from '../views/Skylantern.vue'
+import SkylanternHome from '../views/SkylanternHome.vue'
+import SkylanternStep_1 from '../views/SkylanternStep_1.vue'
+import SkylanternStep_2 from '../views/SkylanternStep_2.vue'
+import SkylanternStep_3 from '../views/SkylanternStep_3.vue'
 
 const router = createRouter({
   history: createWebHashHistory(),
   routes: [
     {
-      path: '/',
+      path: '/:lang?',
       name: 'home',
       component: HomeView,
       children: [
         {
-          path: '/',
+          path: '/:lang?',
           component: Language,
         },
         {
@@ -40,40 +42,63 @@ const router = createRouter({
         {
           path: 'step4',
           component: Step_4,
+          meta: {
+            requiresBgImg: true
+          }
         },
         {
           path: 'step5',
           component: Step_5,
+          meta: {
+            requiresBgImg: true
+          }
+        },
+        {
+          path: 'step6',
+          component: Step_6,
+          meta: {
+            requiresBgImg: true
+          }
         },
         // 天燈
         {
-          path: '/interact',
-          name: 'Interact',
-          component: Interact
+          path: '/skylantern',
+          name: 'skylantern',
+          component: Skylantern
         },
         {
-          path: '/interacthome',
-          name: 'Interact_home',
-          component: InteractHome
+          path: '/skylanternhome',
+          name: 'skylantern_home',
+          component: SkylanternHome
         },
         {
-          path: '/interact_step1',
-          name: 'Interact_step1',
-          component: InteractStep_1
+          path: '/skylantern_step1',
+          name: 'skylantern_step1',
+          component: SkylanternStep_1
         },
         {
-          path: '/interact_step2',
-          name: 'Interact_step2',
-          component: InteractStep_2
+          path: '/skylantern_step2',
+          name: 'skylantern_step2',
+          component: SkylanternStep_2
         },
         {
-          path: '/interact_step3',
-          name: 'Interact_step3',
-          component: InteractStep_3
+          path: '/skylantern_step3',
+          name: 'skylantern_step3',
+          component: SkylanternStep_3
         },
       ],
     },
   ]
 })
 
+router.beforeEach((to, from, next) => {
+  const store = useMainStore();
+  // 檢查是否已選擇背景圖,否則不能進入結果頁(step6)
+  if (to.meta.requiresBgImg && !store.assignBgImg) {
+    next('/step3'); // 返回選擇背景頁面
+  } else {
+    next();
+  }
+});
+
 export default router

+ 9 - 2
src/views/HomeView.vue

@@ -20,7 +20,7 @@ main {
 h3,
 p {
   font-weight: 500;
-  letter-spacing: 4px;
+  letter-spacing: 3px;
   line-height: 1.6;
   text-align: center;
   color: white;
@@ -31,6 +31,12 @@ p {
   padding: 4rem 0 6rem;
   position: relative;
   background-color: var(--sub-color);
+
+  .description {
+    letter-spacing: 2px;
+    color: #b3b3b4;
+    text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
+  }
 }
 
 .main-bg {
@@ -93,7 +99,8 @@ p {
   background-repeat: no-repeat;
   background-position: 85% 50%;
 
-  p {
+  p,
+  li {
     line-height: 1.7;
     letter-spacing: 2px;
     text-align: center;

+ 0 - 27
src/views/Interact.vue

@@ -1,27 +0,0 @@
-<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>

+ 0 - 62
src/views/InteractStep_1.vue

@@ -1,62 +0,0 @@
-<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>
-
-    <Footer url="/interact" back="/interacthome" />
-  </div>
-</template>
-
-<style lang="scss" scoped>
-.lartern-content {
-  height: 100vh;
-  padding: 4rem 10vw 0;
-  background-image: url("@/assets/img/background.webp");
-
-  @media (max-width: 600px) {
-    height: 100%;
-    padding: 9rem 1rem 0;
-  }
-
-  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);
-  }
-
-  .main-btn {
-    margin-bottom: 100px;
-  }
-}
-</style>

+ 30 - 0
src/views/Skylantern.vue

@@ -0,0 +1,30 @@
+<script setup>
+import { useI18n } from "vue-i18n";
+import Footer from "../components/Footer.vue";
+
+const { t, locale } = useI18n();
+</script>
+
+<template>
+  <div class="lartern-content">
+    <h3 class="title">
+      {{ t("skylantern.home.text_1") }} <br />
+      {{ t("skylantern.home.text_2") }} <br />
+      {{ t("skylantern.home.text_3") }}
+    </h3>
+
+    <p>
+      {{ t("skylantern.home.text_4") }} <br />
+      {{ t("skylantern.home.text_5") }} <br />
+      {{ t("skylantern.home.text_6") }}
+    </p>
+
+    <router-link to="/skylantern_step1" class="main-btn mt-15">
+      下一步
+    </router-link>
+  </div>
+
+  <Footer url="/skylanternhome" back="/skylanternhome" />
+</template>
+
+<style lang="scss" scoped></style>

+ 16 - 4
src/views/InteractHome.vue → src/views/SkylanternHome.vue

@@ -1,12 +1,24 @@
 <script setup>
-import { reactive } from "vue";
-import { useRouter } from "vue-router";
-let router = useRouter();
+import { reactive, onMounted } from "vue";
+import { useRoute, useRouter } from "vue-router";
+
+const route = useRoute();
+const router = useRouter();
+
+onMounted(() => {
+  console.log('onMounted');
+  let lang = localStorage.getItem("lang");
+  console.log("lang >>", lang);
+
+  if (lang && lang !== "") {
+    chooseLang(lang);
+  }
+});
 
 function chooseLang(lang) {
   console.log("選擇語言:", lang);
   localStorage.setItem("lang", lang);
-  router.push("/interact");
+  router.push("/skylantern");
 }
 
 let langList = reactive([

+ 92 - 0
src/views/SkylanternStep_1.vue

@@ -0,0 +1,92 @@
+<script setup>
+import { useI18n } from "vue-i18n";
+import Footer from "../components/Footer.vue";
+
+const { t } = useI18n();
+</script>
+
+<template>
+  <div class="lartern-content">
+    <section>
+      <h3>{{ t("skylantern.step1.text_1") }}</h3>
+      <p>
+        {{ t("skylantern.step1.text_2") }}
+      </p>
+    </section>
+
+    <section class="my-5">
+      <h3>{{ t("skylantern.step1.text_3") }}</h3>
+
+      <ul>
+        <li>{{ t("skylantern.step1.text_4") }}</li>
+        <li>{{ t("skylantern.step1.text_5") }}</li>
+        <li>{{ t("skylantern.step1.text_6") }}</li>
+        <li>{{ t("skylantern.step1.text_7") }}</li>
+        <li>{{ t("skylantern.step1.text_8") }}</li>
+        <li>{{ t("skylantern.step1.text_9") }}</li>
+      </ul>
+      <!-- <p>
+        {{ t("skylantern.step1.text_4") }}&nbsp;&nbsp;&nbsp;
+        {{ t("skylantern.step1.text_5") }}&nbsp;&nbsp;&nbsp;
+        {{ t("skylantern.step1.text_6") }}
+      </p>
+      <p>
+        {{ t("skylantern.step1.text_7") }}&nbsp;&nbsp;&nbsp;
+        {{ t("skylantern.step1.text_8") }}&nbsp;&nbsp;&nbsp;
+        {{ t("skylantern.step1.text_9") }}
+      </p> -->
+    </section>
+
+    <section>
+      <h3>{{ t("skylantern.step1.text_10") }}</h3>
+      <p>
+        {{ t("skylantern.step1.text_11") }} <br />
+        {{ t("skylantern.step1.text_12") }} <br />
+        {{ t("skylantern.step1.text_13") }}
+      </p>
+    </section>
+
+    <router-link to="/skylantern_step2" class="main-btn mt-10">
+      下一步
+    </router-link>
+
+    <Footer url="/skylantern" back="/skylanternhome" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.lartern-content {
+  height: 100vh;
+  padding: 4rem 10vw 0;
+  background-image: url("@/assets/img/background.webp");
+
+  @media (max-width: 600px) {
+    height: 100%;
+    padding: 9rem 1rem 0;
+  }
+
+  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);
+  }
+
+  ul {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: center;
+    list-style: none;
+
+    li {
+      margin: 0 35px;
+    }
+  }
+
+  .main-btn {
+    margin-bottom: 100px;
+  }
+}
+</style>

+ 2 - 2
src/views/InteractStep_2.vue → src/views/SkylanternStep_2.vue

@@ -19,7 +19,7 @@ function getValue(state) {
       if (state) {
         skyImageUrl.value = response.data.url;
       } else {
-        router.push("/interact_step3");
+        router.push("/skylantern_step3");
         // window.location = "https://cmm.ai/skylantern/";
       }
     })
@@ -65,7 +65,7 @@ function getValue(state) {
       <button @click="getValue(false)" class="main-btn mb-5">送出</button>
     </div>
 
-    <Footer url="/interact_step1" back="/interacthome" />
+    <Footer url="/skylantern_step1" back="/skylanternhome" />
   </div>
 </template>
 

+ 1 - 1
src/views/InteractStep_3.vue → src/views/SkylanternStep_3.vue

@@ -14,7 +14,7 @@ import Footer from "../components/Footer.vue";
     </p>
   </div>
 
-  <Footer url="/interact_step2" back="/interacthome" />
+  <Footer url="/skylantern_step2" back="/skylanternhome" />
 </template>
 
 <style lang="scss" scoped></style>

+ 1 - 1
src/views/Step_1.vue

@@ -31,7 +31,7 @@ import Marquee from "../components/Marquee.vue";
 
 <style lang="scss" scoped>
 p {
-  font-size: 1.5rem;
+  font-size: 1.25rem;
 
   @media (max-width: 600px) {
     font-size: 1rem;

+ 240 - 91
src/views/Step_3.vue

@@ -2,19 +2,22 @@
 import { ref, reactive, 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 router = useRouter();
 const store = useMainStore();
+const { t, locale } = useI18n();
+
 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 bgImg = reactive({
+//   list: [],
+// });
 
 let assignBgImg = ref("");
 
@@ -22,8 +25,7 @@ function handleBgImg(item) {
   console.log("name", item);
   assignBgImg.value = item;
   store.assignBgImg = item;
-
-  parameter.value.filter((e, index) => {
+  parameterList.value.filter((e, index) => {
     if (e.bg_img === item.bg_img) {
       store.styleNum = index;
     }
@@ -55,7 +57,6 @@ 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);
@@ -85,107 +86,236 @@ const currentPage = computed(
 );
 
 // 測試欄位
-let parameters = reactive({
-  styel_name: "",
-  prompt: "",
-  negative_prompt: "",
-  bg_img: "",
-  styles: ["real"],
-});
+// 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 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([]);
+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);
-    console.log("getParameters", response);
-    console.log("response", response);
-    parameter.value = response.data;
-    console.log("parameter.list", parameter.value);
+    parameterList.value = response.data;
+    console.log("parameterList", parameterList.value);
   } catch (error) {
     console.log("error", error);
   }
 }
 
-async function setParameters() {
-  let url = `${apiUrl}/sd/paprameter`;
-  let getUrl = `${apiUrl}/sd/parameters`;
+// 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 (assignBgImg.value === "") {
+//     alert("尚未選取背景圖");
+//     return;
+//   } else {
+//     parameters.bg_img = assignBgImg.value;
+//   }
 
-  if (!imgFile.value) {
-    alert("尚未上傳人物圖");
-  }
+//   if (!imgFile.value) {
+//     alert("尚未上傳人物圖");
+//   }
 
-  console.log("parameters", parameters);
+//   console.log("parameters", parameters);
 
-  try {
-    let response = await axios.post(url, parameters);
-    console.log("setParameters", response);
+//   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);
+//     if (response.status === 200) {
+//       let getResponse = await axios.get(getUrl);
+//       console.log("getResponse", getResponse);
 
-      // 算圖
-      runImg(getResponse.data.length);
-    }
-  } catch (error) {
-    console.log("error", error);
-  }
-}
+//       // 算圖
+//       runImg(getResponse.data.length);
+//     }
+//   } catch (error) {
+//     console.log("error", error);
+//   }
+// }
 
-let imgLoading = ref(false);
-let imgPath = ref("");
+// 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}`;
+// 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);
+//   // 人物圖
+//   const formData = new FormData();
+//   formData.append("file", imgFile.value);
 
-  try {
-    let response = await axios.post(url, formData);
-    console.log("runImg", response);
+//   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);
-  }
-}
+//     if (response.status === 200) {
+//       imgPath.value = response.data[0].path;
+//       imgLoading.value = false;
+//     }
+//   } catch (error) {
+//     console.log("error", error);
+//   }
+// }
 
 let alertShow = ref(false);
 
@@ -220,13 +350,32 @@ function checkImg() {
         v-for="item in currentPhotos"
         class="bg-img"
       >
-        <img
+        <v-img
+          cover
+          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">
+              <v-progress-circular
+                color="grey-lighten-4"
+                indeterminate
+              ></v-progress-circular>
+            </div>
+          </template>
+        </v-img>
+
+        <p>{{ t(item.title) }}</p>
+
+        <!-- <img
           class="cover"
           :src="`http://172.104.93.163:3219/static/assets/img/bg/${item.bg_img}`"
           alt=""
-        />
-        <!-- {{ bg_img }} -->
-        <p>{{ item.bg_img.replace(".png", "") }}</p>
+        /> -->
+       
+        <!-- <p>{{ item.bg_img.replace(".png", "") }}</p> -->
+
         <img
           v-if="item === assignBgImg"
           class="icon active"
@@ -316,7 +465,7 @@ function checkImg() {
 
       <a @click="checkImg()" href="javascript:;" class="main-btn">下一步</a>
 
-      <!-- <router-link to="/step4" class="main-btn">下一步</router-link> -->
+      <!-- <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">
@@ -375,9 +524,9 @@ function checkImg() {
 //   border-radius: 5px;
 // }
 
-img {
-  width: 100%;
-}
+// img {
+//   width: 100%;
+// }
 
 // .title {
 //   padding-top: 4rem;
@@ -409,7 +558,7 @@ img {
 
     .cover {
       max-width: 100%;
-      width: 30rem;
+      width: 20rem;
       height: 25vh;
       object-fit: cover;
     }

+ 39 - 55
src/views/Step_4.vue

@@ -2,15 +2,17 @@
 import { ref, reactive } 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";
 
+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;
 
-console.log("Step4 store.assignBgImg", store.assignBgImg);
+console.log("step5 store.assignBgImg", store.assignBgImg);
 
 let fileInput = ref(null);
 let imgFile = ref(null);
@@ -60,7 +62,7 @@ async function upload() {
       imgLoading.value = false;
       console.log("store.imgPath", store.imgPath);
 
-      router.push("/step5");
+      router.push("/step6");
     }
   } catch (error) {
     console.log("error", error);
@@ -70,43 +72,40 @@ async function upload() {
 
 <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">
-          明信片製作中… <br />
-          請稍等約 30 秒
-        </p>
-
-        <v-progress-circular
-          :size="70"
-          :width="7"
-          color="white"
-          indeterminate
-        ></v-progress-circular>
+    <v-container
+      class="px-5 px-sm-15 mt-15 d-flex flex-column align-center justify-center"
+    >
+      <div>
+        <v-img
+          cover
+          class="cover ma-5"
+          :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>
+
+        <!-- <img
+          class="cover mb-5"
+          :src="`http://172.104.93.163:3219/static/assets/img/bg/${store.assignBgImg.bg_img}`"
+          alt=""
+        /> -->
+        <p>{{ t(store.assignBgImg.title) }}</p>
+
+        <p
+          class="text-start px-5 my-10 description"
+          v-html="t(store.assignBgImg.description)"
+        ></p>
       </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>
+      <router-link to="/step5" class="main-btn mt-auto">確定</router-link>
     </v-container>
 
     <Footer url="/step3" />
@@ -114,30 +113,15 @@ async function upload() {
 </template>
 
 <style lang="scss" scoped>
-.mdi-camera::before {
-  color: #fff !important;
+.v-container {
+  min-height: 75vh;
 }
 
 .content {
-  height: 100vh;
+  min-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>

+ 298 - 25
src/views/Step_5.vue

@@ -1,56 +1,329 @@
 <script setup>
+import { ref, reactive, onMounted } from "vue";
 import { useMainStore } from "@/stores/store";
+import { useRouter } from "vue-router";
+import axios from "axios";
 import Footer from "../components/Footer.vue";
+import VuePictureCropper, { cropper } from "vue-picture-cropper";
 
+const router = useRouter();
 const store = useMainStore();
-// const apiUrl = import.meta.env.VITE_API_URL;
-// const imgUrl = import.meta.env.VITE_API_IMG_URL;
+const apiUrl = import.meta.env.VITE_API_URL;
+const imgUrl = import.meta.env.VITE_API_IMG_URL;
 
-const shareData = {
-  title: "101 AI明信片",
-  text: "",
-  url: store.imgPath,
-};
+// 測試開始
+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 ready() {
+//   console.log("Cropper is ready.");
+// }
+// 測試結束
 
-let resultMessage = "";
+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);
 
-const share = async () => {
   try {
-    await navigator.share(shareData);
-    resultMessage = "MDN shared successfully";
-  } catch (err) {
-    resultMessage = `Error: ${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("/step6");
+    }
+  } catch (error) {
+    console.log("error", error);
+  }
+}
+
+function remove() {
+  file.value = null;
+  imgFile.value = null;
+  imageUrl.value = null;
+}
+
+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">
-      <img class="w-100 mb-15" :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">
+          明信片製作中… <br />
+          請稍等約 30 秒
+        </p>
+
+        <v-progress-circular
+          :size="70"
+          :width="7"
+          color="white"
+          indeterminate
+        ></v-progress-circular>
+
+        <p class="mt-15">AI 正在迅速地帶您前往該景點</p>
+      </div>
+
+      <div v-else>
+        <p class="title mb-5">請上傳您的相片</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>照相/選擇相片</p>
+            </span>
+          </v-btn>
+        </div>
+
+        <input
+          class="d-none"
+          ref="uploadInput"
+          type="file"
+          accept="image/jpg, image/jpeg, image/png"
+          @change="selectFile"
+        />
 
-      <!-- <p class="text-start mt-5 px-5">
-        位在南部橫貫公路、向陽北方,直線約7公里的高山湖泊嘉明湖,是一座被譽為「高山藍寶石」、「天使的眼淚」的橢圓形湖泊。由翠綠的森林植被所環繞,清澈的湖面映照著天空的湛藍,深不可測,彷若人間仙境般的靜謐。傳說中嘉明湖的形成,是因隕石撞擊地球表面後造成的隕石坑,相當罕見。
-      </p> -->
+        <div v-if="isShowModal" class="mt-5">
+          <VuePictureCropper
+            :boxStyle="{
+              width: '100%',
+              height: '100%',
+              backgroundColor: '#f8f8f8',
+              margin: 'auto',
+            }"
+            :img="pic"
+            :options="{
+              viewMode: 1,
+              dragMode: 'crop',
+              aspectRatio: 16 / 9,
+            }"
+            @ready="ready"
+          />
 
-      <button @click="share()" class="main-btn mt-15">分享相片</button>
+          <div class="mt-5 d-flex justify-end">
+            <v-btn @click="reset" color="grey" variant="flat" class="me-3">
+              重置
+            </v-btn>
+
+            <v-btn @click="getResult" color="primary" variant="flat">
+              裁剪
+            </v-btn>
+          </div>
+        </div>
+
+        <!-- Crop result preview -->
+        <section v-if="result.dataURL" class="section">
+          <div class="preview-img">
+            <img :src="result.dataURL" />
+          </div>
+        </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>
+
+      <!-- <router-link to="/step6" class="main-btn">確定</router-link> -->
+
+      <div class="btn-content">
+        <button @click="remove()" class="main-btn">重新上傳</button>
+        <button @click="upload()" class="main-btn">確定</button>
+      </div>
     </v-container>
-    <Footer url="/step3" />
+
+    <Footer url="/step4" />
   </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 {
+  height: 30vh;
+  img {
+    width: 100%;
+    height: 30vh;
+    object-fit: cover;
+  }
+}
+
 .content {
-  padding: 11rem 0 8rem;
-  height: 100vh;
+  min-height: 100vh;
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: center;
+}
+
+.btn-content {
+  width: 100%;
+  padding: 100px 20px 20px;
+  display: flex;
+  justify-content: center;
+  // position: absolute;
+  // left: 50%;
+  // bottom: 20vw;
+  // transform: translate(-50%, 0);
 
-  p {
-    color: #b3b3b4;
-    text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
+  .main-btn {
+    margin: 10px;
   }
 }
+
+.test {
+  width: 300px;
+  height: 500px;
+  object-fit: cover;
+}
+
+.cut {
+  width: 500px;
+  height: 500px;
+  margin: 30px auto;
+}
 </style>

+ 95 - 0
src/views/Step_6.vue

@@ -0,0 +1,95 @@
+<script setup>
+import { useI18n } from "vue-i18n";
+import { useMainStore } from "@/stores/store";
+import Footer from "../components/Footer.vue";
+
+const { t } = useI18n();
+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明信片", // 文字內容
+};
+
+console.log("shareData", shareData);
+
+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 class="mb-10 img-item">
+        <img 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>
+      <!-- {{ t(`${store.assignBgImg.title}_description`) }} -->
+      <button @click="share()" class="main-btn mt-15">分享相片</button>
+    </v-container>
+    <Footer url="/step5" />
+  </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>