SyuanYu 7 달 전
부모
커밋
714c10b732
8개의 변경된 파일582개의 추가작업 그리고 114개의 파일을 삭제
  1. 102 28
      src/components/Chat.vue
  2. 9 6
      src/components/Navbar.vue
  3. 5 0
      src/language/en.json
  4. 12 7
      src/language/ja.json
  5. 31 26
      src/language/ko.json
  6. 7 2
      src/language/zh.json
  7. 10 45
      src/router/index.js
  8. 406 0
      src/views/BrandSearch.vue

+ 102 - 28
src/components/Chat.vue

@@ -179,8 +179,12 @@ function setBtnValue(val) {
   sendMessage("text");
 }
 
+const inputField = ref(null); // 文字輸入框
+
 // 傳送訊息 (如 type="text" 代表為純文字訊息,不需語音回覆)
 async function sendMessage(type = "") {
+  inputField;
+
   if (userMessage.value === "") {
     return;
   }
@@ -199,6 +203,8 @@ async function sendMessage(type = "") {
       author: "user",
     });
 
+    inputField.value?.blur(); // 移除輸入框焦點
+
     videoLoading.value = true;
     let isVideoCache = await getVideoCache(message); // 判斷使用者問題是否有 Video Cache
 
@@ -356,6 +362,23 @@ function videoPlay() {
   video.value.play();
 }
 
+// 影片播放結束觸發
+const onVideoEnded = () => {
+  console.log("播放點頭影片");
+
+  // 清空音訊
+  if (currentAudio.value) {
+    currentAudio.value.pause();
+    currentAudio.value.currentTime = 0;
+    currentAudio.value = null;
+  }
+
+  // 播放點頭影片
+  videoSrc.value = videoMuteSources.value;
+  videoPlay();
+  isVideoPause.value = true;
+};
+
 // 底部選單
 const menu = ref(null);
 const menuHeight = ref(0);
@@ -381,6 +404,8 @@ const updateMenuHeight = () => {
     if (menu.value) {
       menuHeight.value = menu.value.clientHeight;
       chatLoading.value = false;
+
+      console.log("menuHeight.value", menuHeight.value);
     }
   });
 };
@@ -445,6 +470,8 @@ function chooseLang(lang) {
     sources = videoSources.value;
   } else if (language === "en-us") {
     sources = videoSourcesEn.value;
+  } else if (language === "ja-jp") {
+    sources = videoSourcesJp.value;
   } else {
     sources = videoSources.value;
   }
@@ -1256,7 +1283,7 @@ let locationList = reactive([
       },
       {
         value: "101F排隊處",
-        text: "101f_queue_area",
+        text: "101f_queue",
       },
     ],
     // navigation: [
@@ -1326,7 +1353,6 @@ let arVideoDialog = ref(false);
 async function getArviews(route, text, type = "") {
   let url;
   let lang = getLang();
-  console.log("text >>>", text);
 
   if (type === "garden") {
     console.log("route", route);
@@ -1442,6 +1468,7 @@ function assignMapImg(item) {
 // 動態引入視頻文件
 const videoSources = ref([]); // 開場白影片(中)
 const videoSourcesEn = ref([]); // 開場白影片(英)
+const videoSourcesJp = ref([]); // 開場白影片(日)
 const videoMuteSources = ref([]); // 點頭影片(靜音)
 const videoSpeakSources = ref([]); // 動嘴型影片
 
@@ -1463,6 +1490,9 @@ const loadVideoSources = async () => {
     // "https://cmm.ai/101-ai-chatbot-new/video/start_en_1.mp4",
     "https://cmm.ai/101-ai-chatbot-new/video/start_en_2.mp4",
   ];
+  videoSourcesJp.value = [
+    "https://cmm.ai:9101/static/video_cache/others/final-tmp_2024-08-27_12:33:13.mp4",
+  ];
   videoMuteSources.value = [
     // "https://cmm.ai/101-ai-chatbot-new/video/mute_1.mp4",
     "https://cmm.ai/101-ai-chatbot-new/video/mute_2.mp4",
@@ -1501,6 +1531,8 @@ async function selectCategory(value, index) {
         sources = videoSources.value;
       } else if (lang === "en") {
         sources = videoSourcesEn.value;
+      } else if (lang === "jp") {
+        sources = videoSourcesJp.value;
       }
 
       const randomIndex = Math.floor(Math.random() * sources.length);
@@ -1535,7 +1567,7 @@ async function selectCategory(value, index) {
   //   menuList[0][0].value = "叫出真人客服";
   // }
   else if (value === "附近有什麼") {
-    window.open("https://cmm.ai/101-aiv1/#/brand-search", "_blank"); // 另開頁面
+    window.open("https://cmm.ai/101-aiv2/#/brand-search", "_blank"); // 另開頁面
   } else if (value === "秘境花園觀景台") {
     messages.value.push({
       label: "text",
@@ -1745,18 +1777,18 @@ const menuList = reactive([
     //   text: "customer_show",
     //   value: "叫出真人客服",
     // },
-    { imgSrc: "素材-11.png", text: "what_around", value: "附近有什麼" },
-    { imgSrc: "素材-06.png", text: "service_information", value: "服務資訊" },
-    { imgSrc: "素材-07.png", text: "shopping_discounts", value: "購物及優惠" },
-  ],
-  [
     {
       imgSrc: "素材-08.png",
       text: "observation_deck",
       value: "秘境花園觀景台",
     },
+    { imgSrc: "素材-07.png", text: "shopping_discounts", value: "購物及優惠" },
+    { imgSrc: "素材-06.png", text: "service_information", value: "服務資訊" },
+  ],
+  [
     { imgSrc: "素材-09.png", text: "food_souvenirs", value: "美食/伴手禮" },
     { imgSrc: "素材-10.png", text: "location_guide", value: "位置導引" },
+    { imgSrc: "素材-11.png", text: "what_around", value: "附近有什麼" },
   ],
 ]);
 
@@ -2092,6 +2124,17 @@ async function cutVideo() {
 // 音訊結束後暫停影片播放
 const onAudioEnded = () => {
   video.value.pause();
+
+  // 清空音訊
+  if (currentAudio.value) {
+    currentAudio.value.pause();
+    currentAudio.value.currentTime = 0;
+    currentAudio.value = null;
+  }
+
+  // 播放點頭影片
+  videoSrc.value = videoMuteSources.value;
+  videoPlay();
 };
 
 // 判斷音訊是否為播放狀態
@@ -2479,6 +2522,7 @@ async function messageNotInCache(question, answer) {
       <!-- 語言選單 -->
       <div class="lang-select">
         <v-select
+          density="compact"
           :label="`${t('select_language')}`"
           :items="['中文', 'English', '日本語']"
           variant="solo"
@@ -2486,9 +2530,8 @@ async function messageNotInCache(question, answer) {
         ></v-select>
       </div>
 
-      <video ref="video" preload playsinline>
+      <video ref="video" preload playsinline @ended="onVideoEnded">
         <source :src="videoSrc" type="video/mp4" />
-        <!-- <source src="../assets/video/start_1.mp4" type="video/mp4" /> -->
         Your browser does not support the video tag.
       </video>
 
@@ -2651,8 +2694,9 @@ async function messageNotInCache(question, answer) {
         ref="chatArea"
         class="chat-area"
         :class="{ 'area-open': isRotate, 'hide-menu': hideMenu }"
-        :style="{ paddingBottom: !hideMenu ? menuHeight + 20 + 'px' : '90px' }"
+        :style="{ paddingBottom: menuHeight + 20 + 'px' }"
       >
+        <!-- :style="{ paddingBottom: !hideMenu ? menuHeight + 20 + 'px' : '90px' }" -->
         <div v-for="message in messages" class="message-content">
           <p
             v-if="message.label === 'text'"
@@ -2765,7 +2809,7 @@ async function messageNotInCache(question, answer) {
                   </v-btn>
                 </template>
 
-                <v-list style="max-height: 155px; overflow-y: auto">
+                <v-list style="max-height: 250px; overflow-y: auto">
                   <v-list-item
                     v-for="(item, index) in locationList"
                     :key="index"
@@ -2928,7 +2972,7 @@ async function messageNotInCache(question, answer) {
             <img
               class="map-img"
               :class="{ 'show-anchor': showAnchor }"
-              :src="`../src/assets/img/map/${message.body}.webp`"
+              :src="`https://cmm.ai/101-aiv2/map/${message.body}.webp`"
               alt=""
             />
           </div>
@@ -3459,6 +3503,27 @@ async function messageNotInCache(question, answer) {
 
       <!-- 底部選單 -->
       <div ref="menu" class="menu">
+        <!-- 對話輸入框 -->
+        <form
+          @submit.prevent="sendMessage()"
+          class="chat-inputs"
+          :class="{ 'd-none': !showInput }"
+        >
+          <!-- <button @click="hideMenu = false" class="menu-btn">
+              <img src="../assets/img/icon/素材-02.png" alt="" width="50" />
+            </button> -->
+
+          <input
+            ref="inputField"
+            v-model="userMessage"
+            type="text"
+            :placeholder="t('type_message')"
+          />
+          <button type="submit" class="submit">
+            <img width="20" src="../assets/img/paper-plane-solid.svg" alt="" />
+          </button>
+        </form>
+
         <!-- AI 主播影片 -->
         <!-- <div
         v-show="showAnchor"
@@ -3484,8 +3549,9 @@ async function messageNotInCache(question, answer) {
         </div>
       </div> -->
 
-        <div class="menu-table mb-3" :class="{ 'hide-table': hideMenu }">
-          <table class="mt-3">
+        <!-- <div class="menu-table mb-3" :class="{ 'hide-table': hideMenu }"> -->
+        <div class="menu-table my-5">
+          <table>
             <tbody>
               <tr v-for="(row, rowIndex) in menuList" :key="rowIndex">
                 <td v-for="(item, itemIndex) in row" :key="itemIndex">
@@ -3512,7 +3578,7 @@ async function messageNotInCache(question, answer) {
         </tbody>
       </table> -->
 
-        <div class="d-flex align-center position-relative" style="bottom: 20px">
+        <!-- <div class="d-flex align-center position-relative" style="bottom: 20px">
           <div class="position-absolute">
             <button
               v-if="!showInput"
@@ -3559,16 +3625,12 @@ async function messageNotInCache(question, answer) {
               {{ t("question") }}
             </button>
 
-            <!-- 對話輸入框 -->
             <form
               v-else
               @submit.prevent="sendMessage()"
               class="chat-inputs"
               :class="{ 'd-none': !showInput }"
             >
-              <!-- <button @click="hideMenu = false" class="menu-btn">
-              <img src="../assets/img/icon/素材-02.png" alt="" width="50" />
-            </button> -->
 
               <input
                 v-model="userMessage"
@@ -3584,7 +3646,7 @@ async function messageNotInCache(question, answer) {
               </button>
             </form>
           </div>
-        </div>
+        </div> -->
       </div>
 
       <!-- 廣告輪播 -->
@@ -3673,8 +3735,14 @@ async function messageNotInCache(question, answer) {
   <!-- 掃描視窗 -->
   <v-dialog v-model="qrcodeImgDialog" width="auto">
     <v-card class="pa-5">
-      <v-card-title class="text-center"> 請掃描 QR Code 前往 </v-card-title>
+      <!-- <v-card-title class="text-center">
+        請掃描 QR Code 前往 
+      </v-card-title> -->
       <v-card-text>
+        <p class="text-h5 mb-5" style="font-weight: 600">
+          {{ t("scan_qr_code") }}
+        </p>
+
         <qrcode-vue
           :value="qrcodeImgUrl"
           class="w-100 h-100"
@@ -3737,7 +3805,7 @@ async function messageNotInCache(question, answer) {
   overflow-x: hidden;
 
   .video-content {
-    height: 73vh;
+    height: 55vh;
     display: flex;
     justify-content: center;
     transform: scale(1);
@@ -3747,7 +3815,7 @@ async function messageNotInCache(question, answer) {
 
     video {
       width: 100%;
-      height: 100%;
+      height: 120%;
       position: fixed;
       top: 55px;
       left: 0;
@@ -3806,7 +3874,8 @@ async function messageNotInCache(question, answer) {
   }
 
   .map-img {
-    max-width: 100%;
+    max-width: 70%;
+    margin-bottom: 2rem;
 
     &.show-anchor {
       max-width: 70%;
@@ -3815,7 +3884,7 @@ async function messageNotInCache(question, answer) {
 
   .chat-content {
     width: 100%;
-    height: 65vh;
+    height: 60vh;
     margin-top: -1rem;
     position: relative;
     z-index: 100;
@@ -3866,7 +3935,7 @@ async function messageNotInCache(question, answer) {
       display: flex;
       flex-direction: column;
       background: var(--sub-color);
-      height: 45vh;
+      height: 60vh;
       padding: 0 1em 2em;
       overflow-x: hidden;
       overflow-y: auto;
@@ -3962,6 +4031,7 @@ async function messageNotInCache(question, answer) {
       display: flex;
       padding: 13px 20px;
       background-color: white;
+      box-shadow: 0px -3px 10px rgba(150, 150, 150, 0.7);
 
       input {
         width: 100%;
@@ -4447,7 +4517,7 @@ async function messageNotInCache(question, answer) {
   }
 
   .icon {
-    width: 80px;
+    width: 90px;
 
     @media (max-width: 767px) {
       width: 50px;
@@ -4558,5 +4628,9 @@ async function messageNotInCache(question, answer) {
   bottom: 15px;
   right: 20px;
   width: 150px;
+
+  @media (max-width: 575px) {
+    width: 130px;
+  }
 }
 </style>

+ 9 - 6
src/components/Navbar.vue

@@ -1,16 +1,18 @@
 <script setup>
-import { ref, reactive } from "vue";
 </script>
 
 <template>
   <div class="navbar">
-    <img src="../assets/img/logo.svg" alt="" class="logo" />
+    <router-link to="/" class="d-flex ms-auto">
+      <img src="../assets/img/logo.svg" alt="" class="logo" />
+    </router-link>
   </div>
 </template>
 
 <style lang="scss" scoped>
 .navbar {
   display: flex;
+  align-items: center;
   position: fixed;
   top: 0;
   right: 0;
@@ -18,10 +20,11 @@ import { ref, reactive } from "vue";
   z-index: 50;
   padding: 10px 10px 5px;
   background-color: #fafcf7;
+}
 
-  .logo {
-    margin-left: auto;
-    max-width: 100px;
-  }
+.logo {
+  margin-left: auto;
+  width: 100px;
+  max-width: 100%;
 }
 </style>

+ 5 - 0
src/language/en.json

@@ -3,6 +3,7 @@
   "prologue": "Hello, welcome to use the Taipei 101 Intelligent Customer Service System.",
   "service": "How may I help you?",
   "select_language": "Select Language",
+  "submit": "Submit",
   "cta_buy": "Buy now",
   "cta_url": "Go now",
   "customer_show": "AI Customer Service",
@@ -17,6 +18,7 @@
   "ar_tour": "AR navigation",
   "select_location": "Please select you location for AR navigation",
   "question": "Click me to ask a question",
+  "scan_qr_code": "Please scan the QR code to proceed",
   "qr_code_scan_prompt": "Please scan QR Code to obtain current location",
   "tap_to_start_scan": "Tap to start scanning",
   "current_location": "Current location",
@@ -32,6 +34,8 @@
   "second": "second",
   "stop_recording": "After the conversation is complete,<br>please press the stop button to end the recording.",
   "speech_error": "Speech recognition error. Please re-record.",
+  "return_to_ai_support": "Back to AI Support",
+  "search_brand": "Search Brand",
   "service_info": {
     "title": "Service information",
     "inquiry_prompt": "What are you searching for?<br>Or please enter your question",
@@ -128,6 +132,7 @@
     "all_brands": "All Brands"
   },
   "external": {
+    "all": "All",
     "gifts": "Gifts",
     "dining": "Dining",
     "accommodation": "Accommodation",

+ 12 - 7
src/language/ja.json

@@ -3,6 +3,7 @@
   "prologue": "こんにちは、台北101インテリジェントカスタマーサービスシステムをご利用いただき、ようこそ。",
   "service": "どうすればお手伝いできますか?",
   "select_language": "言語を選択",
+  "submit": "送信",
   "cta_buy": "すぐに購入する",
   "cta_url": "すぐに行く",
   "customer_show": "AIインテリジェントカスタマーサービス",
@@ -17,6 +18,7 @@
   "ar_tour": "ARナビを利用",
   "select_location": "あなたの現在地を選んで、ARナビを利用します。",
   "question": "質問するにはここをクリック",
+  "scan_qr_code": "QRコードをスキャンして進んでください",
   "qr_code_scan_prompt": "QRコードをスキャンして現在位置を取得してください",
   "tap_to_start_scan": "タップしてスキャンを開始",
   "current_location": "現在の位置",
@@ -32,6 +34,8 @@
   "second": "秒",
   "stop_recording": "会話終了後、停止ボタンを押して録音を終了してください",
   "speech_error": "音声認識に誤りがあります。もう一度録音してください。",
+  "return_to_ai_support": "AI サポートに戻る",
+  "search_brand": "ブランドを検索",
   "service_info": {
     "title": "サービス情報",
     "inquiry_prompt": "サービス情報を選択<br>下のメッセージボックスにご質問を入力してください。",
@@ -128,12 +132,13 @@
     "all_brands": "全館品牌"
   },
   "external": {
-    "gifts": "伴手禮",
-    "dining": "餐飲",
-    "accommodation": "住宿",
-    "hairdressing": "美髮",
-    "car_supplies": "汽車用品",
-    "entertainment": "休閒娛樂",
-    "pet_supplies": "寵物用品"
+    "all": "全部",
+    "gifts": "お土産",
+    "dining": "飲食",
+    "accommodation": "宿泊",
+    "hairdressing": "美容院",
+    "car_supplies": "カー用品",
+    "entertainment": "レジャー",
+    "pet_supplies": "ペット用品"
   }
 }

+ 31 - 26
src/language/ko.json

@@ -3,6 +3,7 @@
   "prologue": "안녕하세요. 타이베이 101 지능형 고객 서비스 시스템을 이용해 주셔서 환영합니다.",
   "service": "어떻게 도와드릴까요?",
   "select_language": "언어 선택",
+  "submit": "제출",
   "cta_buy": "바로 구매",
   "cta_url": "즉시 이동",
   "customer_show": "실제 고객 상담원",
@@ -12,26 +13,29 @@
   "food_souvenirs": "맛집/기념품",
   "shopping_discounts": "쇼핑 및 혜택",
   "service_information": "서비스 정보",
-  "what_around": "附近有什麼?",
-  "type_message": "請輸入訊息",
-  "ar_tour": "進行 AR 導覽",
-  "select_location": "請選擇您的位置進行 AR 導覽",
+  "what_around": "주변에 뭐가 있나요?",
+  "type_message": "메시지를 입력하세요",
+  "ar_tour": "AR 투어 시작",
+  "select_location": "AR 투어를 위해 위치를 선택하세요",
   "question": "질문하려면 클릭하세요",
-  "qr_code_scan_prompt": "請掃描 QR Code 取得當前位置",
-  "tap_to_start_scan": "點我開啟掃描",
-  "current_location": "當前位置",
-  "view_floor_plan": "查看平面圖",
-  "select_navigation_location": "請選擇導覽位置",
-  "b1_floor_plan": "B1 平面圖",
-  "1f_floor_plan": "1F 平面圖",
-  "2f_floor_plan": "2F 平面圖",
-  "system_construction": "系統建置中,<br />關閉語音輸入功能。",
-  "tap_to_record": "點選以進行錄音",
-  "close": "關閉",
-  "recording": "錄音中",
-  "second": "秒",
-  "stop_recording": "對話完畢後,請按下停止按鈕結束錄音",
-  "speech_error": "語音辨識有誤,請重新錄製。",
+  "scan_qr_code": "QR 코드를 스캔하여 진행하세요",
+  "qr_code_scan_prompt": "현재 위치를 얻기 위해 QR 코드를 스캔하세요",
+  "tap_to_start_scan": "스캔을 시작하려면 탭하세요",
+  "current_location": "현재 위치",
+  "view_floor_plan": "평면도 보기",
+  "select_navigation_location": "네비게이션 위치를 선택하세요",
+  "b1_floor_plan": "B1 평면도",
+  "1f_floor_plan": "1F 평면도",
+  "2f_floor_plan": "2F 평면도",
+  "system_construction": "시스템 구축 중입니다.<br />음성 입력 기능을 비활성화합니다.",
+  "tap_to_record": "녹음을 시작하려면 탭하세요",
+  "close": "닫기",
+  "recording": "녹음 중",
+  "second": "초",
+  "stop_recording": "대화가 끝나면 정지 버튼을 눌러 녹음을 종료하세요",
+  "speech_error": "음성 인식 오류가 발생했습니다. 다시 녹음해주세요.",
+  "return_to_ai_support": "AI 고객 지원으로 돌아가기",
+  "search_brand": "브랜드 검색",
   "service_info": {
     "title": "服務資訊",
     "inquiry_prompt": "請問您想查詢?",
@@ -128,12 +132,13 @@
     "all_brands": "全館品牌"
   },
   "external": {
-    "gifts": "伴手禮",
-    "dining": "餐飲",
-    "accommodation": "住宿",
-    "hairdressing": "美髮",
-    "car_supplies": "汽車用品",
-    "entertainment": "休閒娛樂",
-    "pet_supplies": "寵物用品"
+    "all": "전체",
+    "gifts": "기념품",
+    "dining": "음식",
+    "accommodation": "숙박",
+    "hairdressing": "미용실",
+    "car_supplies": "자동차 용품",
+    "entertainment": "레저",
+    "pet_supplies": "애완용품"
   }
 }

+ 7 - 2
src/language/zh.json

@@ -3,6 +3,7 @@
   "prologue": "您好,歡迎您使用台北101智能客服系統",
   "service": "請問有什麼可以為您服務的呢?",
   "select_language": "切換語言",
+  "submit": "送出",
   "cta_buy": "立即購票",
   "cta_url": "立即前往",
   "customer_show": "叫出真人客服",
@@ -12,11 +13,12 @@
   "food_souvenirs": "美食/伴手禮",
   "shopping_discounts": "購物及優惠",
   "service_information": "服務資訊",
-  "what_around": "附近有什麼?",
+  "what_around": "101附近有什麼?",
   "type_message": "請輸入訊息",
   "ar_tour": "進行 AR 導覽",
-  "select_location": "請選擇您的位置進行 AR 導覽",
+  "select_location": "請選擇您的位置",
   "question": "點我問問題",
+  "scan_qr_code": "請掃描 QR Code 前往",
   "qr_code_scan_prompt": "請掃描 QR Code 取得當前位置",
   "tap_to_start_scan": "點我開啟掃描",
   "current_location": "當前位置",
@@ -32,6 +34,8 @@
   "second": "秒",
   "stop_recording": "對話完畢後,請按下停止按鈕結束錄音",
   "speech_error": "語音辨識有誤,請重新錄製。",
+  "return_to_ai_support": "返回 AI 客服",
+  "search_brand": "搜尋品牌",
   "service_info": {
     "title": "服務資訊",
     "inquiry_prompt": "請問您想查詢?<br>或於下方文字框輸入您的問題",
@@ -128,6 +132,7 @@
     "all_brands": "全館品牌"
   },
   "external": {
+    "all": "全部",
     "gifts": "伴手禮",
     "dining": "餐飲",
     "accommodation": "住宿",

+ 10 - 45
src/router/index.js

@@ -1,6 +1,7 @@
 import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
 import HomeView from '../views/HomeView.vue'
-import ArTourView from '../views/ArTourView.vue'
+import BrandSearch from '../views/BrandSearch.vue'
+// import ArTourView from '../views/ArTourView.vue'
 
 const router = createRouter({
   history: createWebHashHistory(),
@@ -10,52 +11,16 @@ const router = createRouter({
       name: 'home',
       component: HomeView,
     },
-    // 定位點網址設定
     {
-      path: '/b1-1',
-      component: HomeView,
-    },
-    {
-      path: '/b1-2',
-      component: HomeView,
-    },
-    {
-      path: '/b1-3',
-      component: HomeView,
-    },
-    {
-      path: '/b1-4',
-      component: HomeView,
-    },
-    {
-      path: '/b1-5',
-      component: HomeView,
-    },
-    {
-      path: '/1f-6',
-      component: HomeView,
-    },
-    {
-      path: '/1f-7',
-      component: HomeView,
-    },
-    {
-      path: '/2f-8',
-      component: HomeView,
-    },
-    {
-      path: '/89f-9',
-      component: HomeView,
-    },
-    {
-      path: '/88f-10',
-      component: HomeView,
-    },
-    {
-      path: '/ar-tour',
-      name: 'ar-tour',
-      component: ArTourView
+      path: '/brand-search',
+      name: 'BrandSearch',
+      component: BrandSearch,
     },
+    // {
+    //   path: '/ar-tour',
+    //   name: 'ar-tour',
+    //   component: ArTourView
+    // },
   ]
 })
 

+ 406 - 0
src/views/BrandSearch.vue

@@ -0,0 +1,406 @@
+<script setup>
+import { ref, reactive, watch } from "vue";
+// i18n
+import { useI18n } from "vue-i18n";
+// Axios
+import axios from "axios";
+// Qrcode.vue
+import QrcodeVue from "qrcode.vue";
+// Components
+import Navbar from "../components/Navbar.vue";
+
+const { t } = useI18n();
+
+// QR Code 彈跳視窗
+let qrcodeImgDialog = ref(false);
+let qrcodeImgUrl = ref("");
+
+// 開啟掃描視窗
+function openQrcodeDialog(item) {
+  console.log("item", item);
+  qrcodeImgUrl.value = item;
+  qrcodeImgDialog.value = true;
+}
+
+let loading = ref(false);
+let pageNum = ref(1); // 當前頁數(預設第一頁)
+let pageAmount = ref(10); // 每頁顯示筆數
+let totalPages = ref(1); // 總頁數
+
+let assignBrand = ref("");
+let searchKeyword = ref("");
+let noResults = ref(false);
+
+// 按鈕篩選
+function filterBrand(val) {
+  if (val !== "全部") {
+    assignBrand.value = val;
+  } else {
+    assignBrand.value = "";
+  }
+  getBrand();
+  pageNum.value = 1;
+}
+
+watch(pageNum, () => {
+  getBrand();
+});
+
+let data = reactive({
+  list: [],
+});
+
+let searchDialog = ref(false);
+
+// 取得館外品牌
+async function getBrand() {
+  loading.value = true;
+
+  let keyword = "館外";
+  let url = `https://cmm.ai:9101/find_brand?language=ch&page_num=${pageNum.value}&page_amount=${pageAmount.value}`;
+
+  if (searchKeyword.value !== "") {
+    url += `&search_name=${searchKeyword.value}`;
+  }
+
+  // 指定類別
+  if (assignBrand.value !== "") {
+    keyword += `,${assignBrand.value}`;
+  }
+
+  url += `&keyword=${keyword}`;
+
+  try {
+    const response = await axios.get(url);
+    data.list = response.data.data;
+    totalPages.value = getTotalPages(response.data.all_num, pageAmount.value); // 計算頁數
+    loading.value = false;
+    console.log("館外品牌", response.data.data);
+
+    if (!response.data.data.length) {
+      noResults.value = true;
+    } else {
+      noResults.value = false;
+    }
+
+    searchDialog.value = false;
+    searchKeyword.value = "";
+  } catch (error) {
+    console.error(error);
+  }
+}
+
+getBrand();
+
+// 計算頁數
+function getTotalPages(totalRecords, recordsPerPage) {
+  return Math.ceil(totalRecords / recordsPerPage);
+}
+
+// 截斷文字
+const truncateText = (text, maxLength) => {
+  text = text.replace(/\s+/g, "");
+  if (text.length <= maxLength) {
+    return text;
+  }
+  return text.substring(0, maxLength) + "...";
+};
+
+let brandList = reactive([
+  {
+    value: "全部",
+    text: "external.all",
+  },
+  {
+    value: "伴手禮",
+    text: "external.gifts",
+  },
+  {
+    value: "餐飲",
+    text: "external.dining",
+  },
+  {
+    value: "住宿",
+    text: "external.accommodation",
+  },
+  {
+    value: "美髮",
+    text: "external.hairdressing",
+  },
+  {
+    value: "汽車用品",
+    text: "external.car_supplies",
+  },
+  {
+    value: "休閒娛樂",
+    text: "external.entertainment",
+  },
+  {
+    value: "寵物用品",
+    text: "external.pet_supplies",
+  },
+]);
+</script>
+<template>
+  <Navbar />
+
+  <div class="brand-list">
+    <router-link v-if="!isHome" to="/" class="back-link">
+      <p>< {{ t("return_to_ai_support") }}</p>
+    </router-link>
+
+    <!-- <v-text-field
+      v-model="searchKeyword"
+      append-inner-icon="mdi-magnify"
+      density="compact"
+      :label="`${t('search_brand')}`"
+      variant="outlined"
+      hide-details
+      single-line
+      @click:append-inner="getBrand"
+      @keydown.enter="getBrand"
+      class="border-sm rounded"
+    ></v-text-field> -->
+
+    <v-row class="mt-3 px-1">
+      <v-col
+        cols="6"
+        v-for="(item, index) in brandList"
+        :key="index"
+        class="pa-2"
+      >
+        <button
+          @click="filterBrand(item.value)"
+          :class="{ active: item.value === assignBrand }"
+          class="rounded brand-btn"
+        >
+          {{ t(item.text) }}
+        </button>
+      </v-col>
+
+      <v-col cols="12">
+        <!-- 搜尋按鈕 -->
+        <v-btn class="w-100" @click="searchDialog = true" color="primary">
+          <p class="text-white m-0">
+            {{ t("search_brand") }}
+          </p>
+        </v-btn>
+      </v-col>
+    </v-row>
+
+    <div v-if="loading" class="d-flex justify-center pt-15">
+      <v-progress-circular color="primary" indeterminate></v-progress-circular>
+    </div>
+
+    <v-row v-else-if="!loading && !noResults" class="content mt-0">
+      <v-col
+        cols="12"
+        class="card"
+        v-for="(item, index) in data.list"
+        :key="index"
+      >
+        <!-- <a :href="item.info.url" target="_blank">
+          <img class="card-img" :src="item.info.img" :alt="item.info.name" />
+        </a> -->
+        <img class="card-img" :src="item.info.img" :alt="item.info.name" />
+        <h2>{{ item.info.name }}</h2>
+        <p>{{ truncateText(item.info.content, 100) }}</p>
+        <div class="d-flex align-center mt-3 mb-5">
+          <img src="../assets/img/location-dot-solid.svg" width="15" alt="" />
+          <p class="ms-2">{{ item.info.address }}</p>
+        </div>
+        <div class="cta">
+          <button
+            @click="openQrcodeDialog(item.info.website_url || item.info.url)"
+          >
+            {{ t("cta_url") }}
+          </button>
+          <!-- <a :href="item.info.url" target="_blank">
+            <button>{{ t("cta_url") }}</button>
+          </a> -->
+        </div>
+      </v-col>
+    </v-row>
+
+    <router-link v-if="!isHome" to="/" class="back-link fixed">
+      <p>< {{ t("return_to_ai_support") }}</p>
+    </router-link>
+
+    <p v-else-if="noResults" class="text-center mt-10">
+      101館外找不到符合的資料,請重新搜尋。
+    </p>
+
+    <v-pagination
+      v-if="!loading && !noResults"
+      color="primary"
+      v-model="pageNum"
+      class="my-10"
+      :length="totalPages"
+    ></v-pagination>
+  </div>
+
+  <!-- 搜尋框 -->
+  <v-dialog v-model="searchDialog" width="500">
+    <v-card class="pa-5">
+      <v-card-title class="pa-0">
+        <button @click="searchDialog = false" class="d-flex ml-auto">
+          <v-icon size="small" icon="mdi-close"></v-icon>
+        </button>
+      </v-card-title>
+
+      <v-card-text class="py-3">
+        <div class="d-flex">
+          <v-text-field
+            v-model="searchKeyword"
+            append-inner-icon="mdi-magnify"
+            density="compact"
+            :label="`${t('search_brand')}`"
+            variant="outlined"
+            hide-details
+            single-line
+            @click:append-inner="getBrand"
+            @keydown.enter="getBrand"
+            class="border-sm rounded mr-5"
+          ></v-text-field>
+
+          <v-btn @click="getBrand" color="primary" style="margin-top: 3px">
+            {{ t("submit") }}
+          </v-btn>
+        </div>
+      </v-card-text>
+    </v-card>
+  </v-dialog>
+
+  <!-- 掃描視窗 -->
+  <v-dialog v-model="qrcodeImgDialog" width="auto">
+    <v-card class="pa-5">
+      <!-- <v-card-title class="text-center">
+        請掃描 QR Code 前往 
+      </v-card-title> -->
+      <v-card-text class="d-flex flex-column align-center">
+        <p class="text-h5 mb-5" style="font-weight: 600">
+          {{ t("scan_qr_code") }}
+        </p>
+
+        <qrcode-vue
+          :value="qrcodeImgUrl"
+          class="w-100 h-100"
+          style="max-width: 300px"
+        />
+      </v-card-text>
+    </v-card>
+  </v-dialog>
+</template>
+
+<style lang="scss">
+.brand-list {
+  width: 90%;
+  margin: 80px auto 30px;
+
+  .back-link {
+    display: block;
+    margin-left: 0.5rem;
+    text-decoration: none;
+    color: var(--main-color);
+    font-weight: 700;
+    letter-spacing: 1px;
+
+    &.fixed {
+      width: 100%;
+      display: flex;
+      align-items: center;
+      padding: 1rem 1.5rem;
+      position: fixed;
+      bottom: 0;
+      left: 0;
+      right: 0;
+      background-color: #fff;
+
+      p {
+        margin-bottom: 0;
+      }
+    }
+
+    p {
+      font-weight: 600;
+      margin-bottom: 25px;
+    }
+  }
+
+  .brand-btn {
+    width: 100%;
+    padding: 0.7rem;
+    color: var(--main-color);
+    background-color: #f5f2eb;
+    letter-spacing: 1px;
+    transition: all 0.3s;
+
+    &:hover {
+      opacity: 0.8;
+    }
+
+    &.active {
+      color: #fff;
+      background-color: var(--main-color);
+    }
+  }
+
+  .content {
+    text-align: center;
+
+    .card {
+      border-bottom: 1px solid #cea84d;
+      padding: 2rem 1rem;
+
+      &:last-child {
+        border-bottom: none !important;
+      }
+    }
+
+    .card {
+      letter-spacing: 0.05rem;
+
+      h2 {
+        margin: 0.5rem 0;
+        font-size: 1.2rem;
+        font-weight: bold;
+        text-align: left;
+      }
+
+      p {
+        text-align: left;
+      }
+
+      .card-img {
+        width: 100%;
+        height: 220px;
+        object-fit: contain;
+      }
+
+      .cta {
+        text-align: right;
+
+        button {
+          padding: 10px 20px;
+          border-radius: 100px;
+          color: #fff;
+          text-align: center;
+          background-color: #b07843;
+          letter-spacing: 0.05rem;
+          text-decoration: none;
+          transition: all 0.3s;
+        }
+      }
+    }
+  }
+
+  .v-pagination {
+    margin: auto;
+    max-width: 300px;
+  }
+
+  .v-btn--icon.v-btn--density-default {
+    width: 35px !important;
+  }
+}
+</style>