Ver código fonte

Merge branch 'front-dev' of ai-anchor/video-maker into master

tomoya 1 ano atrás
pai
commit
1805562521

+ 12 - 9
frontend/src/App.vue

@@ -1,11 +1,3 @@
-<template>
-  <v-app>
-    <LoadingView v-if="loggedIn === null" />
-    <RouterView v-else />
-    <NotificationsManager />
-  </v-app>
-</template>
-
 <script setup lang="ts">
 import { RouterView } from "vue-router";
 import { useMainStore } from "@/stores/main";
@@ -22,10 +14,21 @@ const loggedIn = mainStoreRef.readIsLoggedIn;
 
 //lifecycle
 onMounted(() => {
-  mainStore.checkLoggedIn();
+  let path = location.pathname;
+  if (path !== "/qrcode") {
+    mainStore.checkLoggedIn();
+  }
 });
 </script>
 
+<template>
+  <v-app>
+    <LoadingView v-if="loggedIn === null" />
+    <RouterView v-else />
+    <NotificationsManager />
+  </v-app>
+</template>
+
 <style lang="scss">
 :root {
   --main-color: #ea5413;

+ 24 - 8
frontend/src/api.ts

@@ -18,6 +18,29 @@ export const api = {
 
     return axios.post(`${apiUrl}/api/v1/login/access-token`, params);
   },
+  async qrLogInGetToken(username: string, password: string,ser_no: string) {
+    const params = new URLSearchParams();
+    params.append("username", username);
+    params.append("password", password);
+
+    return axios.post(`${apiUrl}/api/v1/login/access-token?add_time_code=${ser_no}`, params);
+  },
+  async googleLogin(username: string) {
+    const params = new URLSearchParams();
+    params.append("username", username);
+    params.append("password", "google");
+
+    return axios.post(`${apiUrl}/api/v1/login/google/access-token`, params);
+  },
+  async qrGoogleLogin(username: string,ser_no: string) {
+    const params = new URLSearchParams();
+    params.append("username", username);
+    params.append("password", "google");
+    return axios.post(`${apiUrl}/api/v1/login/google/access-token?add_time_code=${ser_no}`, params);
+  },
+  async qrAddTime(token: string, code: string) {
+    return axios.get(`${apiUrl}/api/v1/ser_nos/add-time?code=${code}`, authHeaders(token));
+  },
   async getMe(token: string) {
     return axios.get<IUserProfile>(`${apiUrl}/api/v1/users/me`, authHeaders(token));
   },
@@ -113,12 +136,5 @@ export const api = {
   },
   async getVideos(token: string) {
     return axios.get<Video[]>(`${apiUrl}/api/v1/videos/`, authHeaders(token));
-  },
-  async googleLogin(username: string) {
-    const params = new URLSearchParams();
-    params.append("username", username);
-    params.append("password", "google");
-
-    return axios.post(`${apiUrl}/api/v1/login/google/access-token`, params);
-  },
+  }
 };

BIN
frontend/src/assets/img/qrcode/Angela.png


BIN
frontend/src/assets/img/qrcode/aianchor3-title.png


BIN
frontend/src/assets/img/qrcode/aianchor3bg.png


BIN
frontend/src/assets/img/qrcode/angela.webp


BIN
frontend/src/assets/img/qrcode/button1.png


BIN
frontend/src/assets/img/qrcode/button2.png


BIN
frontend/src/assets/img/qrcode/button3.png


BIN
frontend/src/assets/img/qrcode/choozmologo.png


BIN
frontend/src/assets/img/qrcode/icon-19.png


BIN
frontend/src/assets/img/qrcode/icon-20.png


BIN
frontend/src/assets/img/qrcode/line1.png


BIN
frontend/src/assets/img/qrcode/line2.png


BIN
frontend/src/assets/img/qrcode/moichiu.png


BIN
frontend/src/assets/img/qrcode/moichiu.webp


BIN
frontend/src/assets/img/qrcode/startline.png


BIN
frontend/src/assets/img/qrcode/startline1.png


BIN
frontend/src/assets/img/qrcode/儲值卷-06.png


BIN
frontend/src/assets/img/qrcode/儲值卷-07.png


BIN
frontend/src/assets/img/qrcode/儲值卷-13.png


+ 14 - 4
frontend/src/components/Dialog.vue

@@ -17,6 +17,14 @@ const props = defineProps({
   state: {
     type: String,
   },
+  icon: {
+    type: String,
+    default: "info"
+  },
+  qrcode: {
+    type: Boolean,
+    default: false,
+  }
 });
 
 const emit = defineEmits(["close"]);
@@ -35,12 +43,14 @@ function close() {
     <v-card>
       <v-card-text>
         <section class="d-flex flex-column align-center">
-          <v-icon style="font-size: 70px" icon="info" :color="state" />
-          <p class="mt-3">{{ msg }}</p>
+          <v-icon style="font-size: 70px" :icon="icon" :color="state" />
+          <!-- <p class="mt-3">{{ msg }}</p> -->
+          <p  v-html="msg" class="mt-3 text-center"></p>
         </section>
       </v-card-text>
-      <v-card-actions>
-        <v-btn @click="close()" class="mx-auto"> {{ t("close") }}</v-btn>
+      <v-card-actions class="d-flex justify-space-evenly">
+        <v-btn @click="close()" v-show="!qrcode || (state === 'error' && qrcode)">{{ t("close") }}</v-btn>
+        <router-link v-show="(state === 'success' || state === 'error') && qrcode" to="/main/dashboard">返回使用者頁面</router-link>
       </v-card-actions>
     </v-card>
   </v-dialog>

+ 1 - 0
frontend/src/components/Navbar.vue

@@ -12,6 +12,7 @@ let lang = reactive([
 let menu = reactive([
   { title: "login", link: "/login" },
   { title: "register", link: "/signup" },
+  // { title: "實體卡儲值", link: "/qrcode" },
 ]);
 
 function setLang(lang: String) {

+ 1 - 1
frontend/src/interfaces/index.ts

@@ -44,7 +44,7 @@ export interface MainState {
 export interface Video {
   id: number;
   title: string;
-  stored_file_name: string;
+  stored_filename: string;
   progress_state: string;
 }
 

+ 12 - 7
frontend/src/router/index.ts

@@ -6,10 +6,10 @@ const router = createRouter({
   history: createWebHistory(import.meta.env.BASE_URL),
   routes: [
     {
-      path: "/",
-      name:'/',
+      path: '/',
+      name: '/',
       component: () => import(/* webpackChunkName: "start" */ '@/views/main/Start.vue'),
-      children:[
+      children: [
         {
           path: 'login',
           name: 'login',
@@ -33,6 +33,11 @@ const router = createRouter({
           name: 'reset-password',
           component: () => import(/* webpackChunkName: "reset-password" */ '@/views/ResetPassword.vue'),
         },
+        {
+          path: 'qrcode',
+          name: 'qrcode',
+          component: () => import(/* webpackChunkName: "reset-password" */ '@/views/Qrcode.vue'),
+        },
         {
           path: 'main',
           name: 'main',
@@ -46,22 +51,22 @@ const router = createRouter({
             {
               path: 'make-video',
               name: 'make-video',
-              component: () => import ('@/views/main/Upload.vue'),
+              component: () => import('@/views/main/Upload.vue'),
             },
             {
               path: 'make-article',
               name: 'make-article',
-              component: () => import ('@/views/main/Article.vue'),
+              component: () => import('@/views/main/Article.vue'),
             },
             {
               path: 'make-image',
               name: 'make-image',
-              component: () => import ('@/views/main/Image.vue'),
+              component: () => import('@/views/main/Image.vue'),
             },
             {
               path: 'progress',
               name: 'progress',
-              component: () => import ('@/views/main/Progress.vue'),
+              component: () => import('@/views/main/Progress.vue'),
             },
             {
               path: 'profile',

+ 69 - 20
frontend/src/stores/main.ts

@@ -75,6 +75,51 @@ export const useMainStore = defineStore("MainStoreId", {
         await this.logOut();
       }
     },
+    async qrLogIn(username: string, password: string, ser_no: string) {
+      try {
+        const response = await api.qrLogInGetToken(username, password, ser_no);
+        return response;
+      } catch (error) {
+        console.log('error', error);
+      }
+    },
+    async googleLogin(username: string) {
+      try {
+        const response = await api.googleLogin(username);
+        const token: string = response.data.access_token;
+        if (token) {
+          saveLocalToken(token);
+          this.setToken(token);
+          this.setLoggedIn(true);
+          this.setLogInError(false);
+          await this.getUserProfile();
+          await this.routeLoggedIn();
+          this.addNotification({ content: i18n.global.t("loggedIn"), color: "success" });
+        } else {
+          await this.logOut();
+        }
+      } catch (err) {
+        this.setLogInError(true);
+        await this.logOut();
+      }
+    },
+    async qrGoogleLogin(username: string, ser_no: string) {
+      try {
+        const response = await api.qrGoogleLogin(username, ser_no);
+        return response;
+      } catch (error) {
+        console.log('error', error);
+      }
+    },
+    async qrAddTime(code: string) {
+      try {
+        const response = await api.qrAddTime(this.token, code);
+        return response;
+      } catch (error) {
+        console.log('error',error);
+        return error;
+      }
+    },
     async getUserProfile() {
       try {
         const response = await api.getMe(this.token);
@@ -129,6 +174,30 @@ export const useMainStore = defineStore("MainStoreId", {
         }
       }
     },
+    async qrCheckLoggedIn() {
+      if (!this.isLoggedIn) {
+        let token = this.token;
+        if (!token) {
+          const localToken = getLocalToken();
+          if (localToken) {
+            this.setToken(localToken);
+            token = localToken;
+          }
+        }
+        if (token) {
+          try {
+            const response = await api.getMe(token);
+            this.setLoggedIn(true);
+            this.setUserProfile(response.data);
+            // router.push("/main/dashboard");
+          } catch (error) {
+            await this.removeLogIn();
+          }
+        } else {
+          await this.removeLogIn();
+        }
+      }
+    },
     async removeLogIn() {
       removeLocalToken();
       this.setToken("");
@@ -368,26 +437,6 @@ export const useMainStore = defineStore("MainStoreId", {
       } catch (error) {
         await mainStore.checkApiError(error);
       }
-    },
-    async googleLogin(username: string) {
-      try {
-        const response = await api.googleLogin(username);
-        const token: string = response.data.access_token;
-        if (token) {
-          saveLocalToken(token);
-          this.setToken(token);
-          this.setLoggedIn(true);
-          this.setLogInError(false);
-          await this.getUserProfile();
-          await this.routeLoggedIn();
-          this.addNotification({ content: i18n.global.t("loggedIn"), color: "success" });
-        } else {
-          await this.logOut();
-        }
-      } catch (err) {
-        this.setLogInError(true);
-        await this.logOut();
-      }
     }
   }
 });

+ 766 - 0
frontend/src/views/Qrcode.vue

@@ -0,0 +1,766 @@
+<script setup lang="ts">
+import { ref, reactive, computed, onMounted } from "vue";
+import { useMainStore } from "@/stores/main";
+import { useDisplay } from "vuetify";
+import { useRoute } from "vue-router";
+import { useI18n } from "vue-i18n";
+import { googleTokenLogin, decodeCredential } from "vue3-google-login";
+import type { CallbackTypes } from "vue3-google-login";
+// import Navbar from "@/components/Navbar.vue";
+import Dialog from "@/components/Dialog.vue";
+
+const mainStore = useMainStore();
+
+// variable
+const { t } = useI18n();
+const route = useRoute();
+const { name } = useDisplay();
+const email = ref("");
+const password = ref("");
+let ser_no: any = ref("");
+let showPassword = ref(false);
+let loginState = ref(false);
+let loading = ref(false);
+let dialog = reactive({
+  msg: "",
+  state: "",
+  show: false,
+  icon: "check_circle",
+});
+
+// getter
+const width = computed(() => {
+  switch (name.value) {
+    case "xs":
+      return 12;
+    case "sm":
+      return 8;
+  }
+});
+
+function setDialog(status: Boolean, msg: String = "") {
+  if (status) {
+    dialog.show = true;
+    dialog.state = "success";
+    dialog.msg =
+      "儲值成功!<br/>已獲得價值 1000 元的 120 秒影片製作時間<br/>(儲值成功後即可登入電腦版進行影片製作)";
+    dialog.icon = "check_circle";
+  } else {
+    dialog.show = true;
+    dialog.state = "error";
+    dialog.msg = `${msg}`;
+    dialog.icon = "highlight_off";
+  }
+}
+
+// action
+async function submit() {
+  loading.value = true;
+  if (email.value === "" || password.value === "") {
+    loading.value = false;
+    return;
+  }
+
+  let response = await mainStore.qrLogIn(
+    email.value,
+    password.value,
+    ser_no.value
+  );
+
+  loading.value = false;
+
+  if (response?.data.time_added === -1) {
+    setDialog(false, "此序號無效");
+  } else if (response?.status === 200) {
+    setTimeout(() => {
+      setDialog(true);
+    }, 500);
+  }
+}
+
+// lifecycle
+onMounted(() => {
+  console.log("onMounted");
+  if (route.query["add_time_code"]) {
+    ser_no.value = route.query["add_time_code"];
+  }
+  mainStore.qrCheckLoggedIn();
+  if (mainStore.token) {
+    checkCode("");
+  } else {
+    loginState.value = false;
+  }
+
+  async function checkCode(method: string = "") {
+    loading.value = true;
+    let response: any = await mainStore.qrAddTime(ser_no.value);
+    loading.value = false;
+    if (response.status === 200) {
+      loginState.value = true;
+      setDialog(true);
+    } else if (response.response.status === 400) {
+      setDialog(
+        false,
+        "此序號已被使用 <br> This serial number is already used"
+      );
+    }
+  }
+
+  // if (route.params.ser_no) {
+  //   ser_no.value = route.params.ser_no;
+  //   console.log("ser_no.value", ser_no.value);
+  // }
+});
+
+const callback: CallbackTypes.CredentialCallback = async (response: any) => {
+  loading.value = true;
+  const userData: any = decodeCredential(response.credential);
+  let res = await mainStore.qrGoogleLogin(userData.email, ser_no.value);
+  loading.value = false;
+  if (res?.data.time_added === -1) {
+    setDialog(false, "此序號無效");
+  } else if (res?.status === 200) {
+    setTimeout(() => {
+      setDialog(true);
+    }, 500);
+  }
+};
+</script>
+
+<template>
+  <!-- <Navbar /> -->
+  <v-container fluid class="pa-0 overflow-hidden">
+    <div class="ai_anchor3_content">
+      <div class="ai_anchor3_content_box">
+        <img
+          class="ai_anchor3_content_title img-fluid"
+          src="../assets/img/qrcode/aianchor3-title.png"
+          alt=""
+        />
+        <h1>集仕多股份有限公司</h1>
+        <h1>AI 主播&ensp;儲值禮物卡</h1>
+      </div>
+      <div class="ai_anchor3_content_start">
+        <!-- <h1>開放倒數</h1> -->
+        <img
+          class="img-fluid"
+          src="../assets/img/qrcode/startline.png"
+          alt=""
+        />
+      </div>
+
+      <v-row
+        align="center"
+        justify="center"
+        no-gutters
+        class="overflow-hidden mx-auto login-form"
+        v-if="!loginState"
+      >
+        <v-col cols="12" class="px-6 my-8 my-md-0">
+          <div class="form-title">
+            <h3>登入後即可獲得儲值金</h3>
+            <span></span>
+          </div>
+          <v-form ref="form" lazy-validation>
+            <v-text-field
+              v-model="email"
+              name="email"
+              prepend-icon="person"
+              :rules="[(v) => !!v || '請輸入您的帳號']"
+              :label="$t('emailAddress')"
+              required
+            >
+            </v-text-field>
+
+            <v-text-field
+              v-model="password"
+              name="password"
+              id="password"
+              prepend-icon="key"
+              :append-icon="showPassword ? 'visibility' : 'visibility_off'"
+              :rules="[(v) => !!v || '請輸入您的密碼']"
+              :type="showPassword ? 'text' : 'password'"
+              :label="$t('password')"
+              hint="4-12 位數密碼"
+              @click:append="showPassword = !showPassword"
+              @keyup.enter="submit"
+              required
+            >
+            </v-text-field>
+
+            <!-- <p class="text-center">
+              {{ t("haventAccount") }}
+              <router-link to="/signup">{{ t("register") }}</router-link> /
+              <router-link to="/recover-password">{{
+                t("forgotPsd")
+              }}</router-link>
+            </p> -->
+
+            <div class="d-flex flex-column">
+              <v-btn rounded="pill" @click.prevent="submit" class="login-btn">
+                {{ t("loginLink") }}
+              </v-btn>
+
+              <section class="line">
+                <p class="d-none d-sm-block">
+                  沒有帳號嗎?使用 Google 快速註冊
+                </p>
+                <p class="d-block d-sm-none">
+                  沒有帳號嗎? <br />
+                  使用 Google 快速註冊
+                </p>
+                <!-- <span></span> -->
+              </section>
+
+              <div class="mx-auto mt-5" style="max-width: 235px">
+                <GoogleLogin
+                  :callback="callback"
+                  prompt
+                  popup-type="TOKEN"
+                  class="w-100"
+                />
+              </div>
+            </div>
+          </v-form>
+        </v-col>
+      </v-row>
+
+      <div class="ai_anchor_moichiu left-70" style="background: #67b5b5">
+        <v-row align="center" no-gutters class="px-0 mx-0">
+          <v-col cols="12" sm="4">
+            <div class="line1">
+              <img
+                class="img-fluid anchor_moichiu"
+                src="../assets/img/qrcode/moichiu.webp"
+                alt=""
+              />
+              <img
+                class="ai_anchor_line1"
+                src="../assets/img/qrcode/line1.png"
+                alt=""
+              />
+            </div>
+            <div class="anchor_name">
+              <img
+                class="img-fluid"
+                src="../assets/img/qrcode/moichiu.png"
+                alt=""
+              />
+            </div>
+          </v-col>
+          <v-col :cols="width">
+            <div class="ai_anchor_moichiu_text">
+              <p>To. 親愛的 VIP</p>
+              <p>我知道您們已經準備好,開始使用 AI 主播的系統了</p>
+              <p>儲值金還在來的路上</p>
+              <!-- <p>在此之前,系統目前測試中</p> -->
+              <p>預計 2023/04/06 後開始正式上線!</p>
+            </div>
+          </v-col>
+        </v-row>
+      </div>
+      <!-- <div class="mt-100">
+        <div class="ai_anchor_moichiu" style="background: #ce96c1">
+          <v-row align="center" no-gutters class="px-0 mx-0">
+            <v-col :cols="width">
+              <div class="ai_anchor_moichiu_text">
+                <p>如果有任何不清楚的,歡迎與我們聯繫</p>
+                <p>AI 主播系統裡會有使用教學</p>
+                <p>現在即可開始使用</p>
+                <p>先登入系統,看看製作影片還需要哪些東西吧!</p>
+                <p>感謝您的耐心等候</p>
+              </div>
+            </v-col>
+            <v-col cols="4">
+              <div class="line2">
+                <img
+                  class="img-fluid anchor_angela"
+                  src="../assets/img/qrcode/angela.webp"
+                  alt=""
+                />
+                <img
+                  class="ai_anchor_line2"
+                  src="../assets/img/qrcode/line2.png"
+                  alt=""
+                />
+              </div>
+
+              <div class="anchor_name2">
+                <img
+                  class="img-fluid"
+                  src="../assets/img/qrcode/Angela.png"
+                  alt=""
+                />
+              </div>
+            </v-col>
+          </v-row>
+        </div>
+      </div> -->
+
+      <div class="progress-item text-center">
+        <v-progress-circular
+          indeterminate
+          color="primary"
+          :size="50"
+          v-if="loading"
+        ></v-progress-circular>
+      </div>
+
+      <div class="CTA_Button_div text-center">
+        <button type="button" class="CTA_Button">
+          <!-- 點我開始製作 AI 主播!<br />Log In -->
+          AI 三代主播系統 <br />
+          於 2023/04/06 正式上線!
+        </button>
+      </div>
+
+      <div class="CTA_box">
+        <a href="https://ai.choozmo.com/ai-presenter/info/" target="_blank">
+          <div class="cta-content">看更多<br />官網介紹</div>
+        </a>
+
+        <a href="https://ai.choozmo.com/contact/service/" target="_blank">
+          <div class="cta-content">
+            聯絡我們
+            <img class="icon20" src="../assets/img/qrcode/icon-19.png" alt="" />
+          </div>
+        </a>
+
+        <a href="https://line.me/R/ti/p/@choozmo?from=page" target="_blank">
+          <div class="cta-content">
+            關注我們
+            <img class="icon20" src="../assets/img/qrcode/icon-20.png" alt="" />
+          </div>
+        </a>
+      </div>
+
+      <div class="logo_box">
+        <img
+          class="choozmologo"
+          src="../assets/img/qrcode/choozmologo.png"
+          alt=""
+        />
+      </div>
+    </div>
+    <Dialog
+      :msg="dialog.msg"
+      :state="dialog.state"
+      :dialog="dialog.show"
+      :icon="dialog.icon"
+      :qrcode="true"
+      @close="dialog.show = false"
+    ></Dialog>
+  </v-container>
+</template>
+
+<style lang="scss" scoped>
+.ai_anchor3_content {
+  background-image: url("../assets/img/qrcode/aianchor3bg.png");
+  width: 100%;
+  background-size: cover;
+  background-repeat: no-repeat;
+}
+.login-form {
+  color: #fff;
+  margin-bottom: 100px;
+  .line {
+    margin-top: 50px;
+    display: flex;
+    justify-content: center;
+    position: relative;
+    p {
+      position: relative;
+      z-index: 1;
+      color: #fff;
+      letter-spacing: 1px;
+      font-size: 24px;
+      font-weight: bold;
+      text-align: center;
+      @media (max-width: 600px) {
+        font-size: 18px;
+      }
+      &::after,
+      &::before {
+        content: "";
+        display: block;
+        height: 1px;
+        width: 50px;
+        background: #fff;
+        position: absolute;
+        bottom: 18px;
+        @media (max-width: 600px) {
+          bottom: 20px;
+        }
+      }
+      &::after {
+        left: -55px;
+      }
+      &::before {
+        right: -55px;
+      }
+    }
+  }
+  .login-btn {
+    color: #fff;
+    background: #4f4fa0;
+  }
+
+  .form-title span {
+    background: #fff !important;
+  }
+}
+.logo_box {
+  text-align: end;
+  overflow: hidden;
+}
+.choozmologo {
+  width: 500px;
+  margin-right: -50px;
+  margin-bottom: -80px;
+  @media (max-width: 767px) {
+    width: 80vw;
+  }
+  @media (max-width: 376px) {
+    width: 85vw;
+  }
+}
+.cta-content {
+  width: 200px;
+  height: 130px;
+  margin: 0 20px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  border: 3px solid #fff;
+  padding: 30px 20px;
+  color: #fff;
+  border-radius: 10px;
+  text-align: center;
+  font-size: 24px;
+  @media (max-width: 767px) {
+    width: 100%;
+    margin: 5vh auto 0;
+  }
+  @media (max-width: 376px) {
+    width: 187px;
+  }
+}
+.icon20 {
+  width: 50px;
+}
+.CTA_box {
+  width: 50%;
+  margin: 100px auto;
+  display: flex;
+  justify-content: space-evenly;
+  @media (max-width: 1200px) {
+    width: 70%;
+  }
+  @media (max-width: 767px) {
+    flex-direction: column;
+  }
+}
+.CTA_Button_div {
+  margin-top: 100px;
+}
+.CTA_Button {
+  width: 400px;
+  height: 120px;
+  border-radius: 50px;
+  background: #4f4fa0;
+  transition: all 300ms ease-in-out;
+  box-shadow: 1px 15px #2e3287;
+  color: #fff;
+  border: none;
+  padding: 0px 30px;
+  font-size: 24px;
+  position: relative;
+  bottom: 0px;
+  left: 0px;
+  transition: all 300ms ease-in-out;
+  font-weight: 600;
+  letter-spacing: 2px;
+  &:hover {
+    bottom: -10px;
+    box-shadow: none;
+  }
+  @media (max-width: 991px) {
+    width: 80%;
+  }
+
+  @media (max-width: 767px) {
+    width: 80%;
+    margin-top: 20vh;
+  }
+  @media (max-width: 575px) {
+    margin-top: 25vh;
+    font-size: 20px;
+  }
+}
+.ai_anchor3_content_box {
+  width: 50vw;
+  margin: 0 auto;
+  padding-top: 150px;
+  text-align: center;
+  color: #fff;
+
+  h1 {
+    font-size: 2rem;
+  }
+  @media (max-width: 991px) {
+    width: 80vw;
+  }
+  @media (max-width: 767px) {
+    width: 90vw;
+  }
+}
+.ai_anchor_moichiu_text {
+  @media (max-width: 767px) {
+    height: 300px;
+    padding: 0;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+  }
+}
+
+.ai_anchor3_content_start {
+  padding-top: 50px;
+  width: 93%;
+  margin: 0 auto;
+  text-align: center;
+  color: #fff;
+  margin-bottom: 15px;
+  h1 {
+    margin-bottom: -60px;
+  }
+}
+.left-70 {
+  left: 70px;
+  @media (max-width: 991px) {
+    left: 0px;
+  }
+}
+.mt-100 {
+  margin-top: 100px;
+
+  @media (max-width: 767px) {
+    margin-top: 25vh;
+  }
+  @media (max-width: 400px) {
+    margin-top: 35vh;
+  }
+}
+.ai_anchor_moichiu {
+  width: 75%;
+  margin: auto;
+  border-radius: 1rem;
+  padding: 50px 10px;
+  position: relative;
+  @media (max-width: 991px) {
+    width: 80%;
+    padding: 40px 10px;
+  }
+  @media (max-width: 767px) {
+    padding: 20px 10px;
+  }
+  @media (max-width: 767px) {
+    width: 90%;
+  }
+  .anchor_moichiu {
+    margin-left: -60px;
+
+    @media (max-width: 767px) {
+      margin-bottom: 0px;
+      margin-top: 0;
+      position: absolute;
+      width: 50vw;
+      left: 53px;
+      bottom: -134px;
+    }
+  }
+
+  .anchor_moichiu,
+  .anchor_angela {
+    width: 350px;
+    max-width: unset;
+    @media (max-width: 1200px) {
+      width: 300px;
+    }
+    @media (max-width: 991px) {
+      width: 275px;
+    }
+  }
+  .anchor_angela {
+    @media (max-width: 767px) {
+      margin-bottom: 0px;
+      margin-top: 0;
+      position: absolute;
+      right: -60vw;
+      bottom: -103px;
+    }
+  }
+  .anchor_name {
+    position: absolute;
+    bottom: -20px;
+    left: -8px;
+    width: 80px;
+
+    @media (max-width: 991px) {
+      width: 50px;
+      left: -8px;
+      bottom: -20px;
+    }
+    @media (max-width: 767px) {
+      left: 250px;
+      bottom: -170px;
+    }
+
+    img {
+      @media (max-width: 1200px) {
+        max-width: unset;
+        width: 70px;
+      }
+      @media (max-width: 991px) {
+        max-width: unset;
+        width: 65px;
+      }
+    }
+  }
+
+  .anchor_name2 {
+    position: absolute;
+    bottom: -5px;
+    right: 0;
+    width: 100px;
+    @media (max-width: 991px) {
+      bottom: -8px;
+      right: -3px;
+      width: 80px;
+    }
+    @media (max-width: 767px) {
+      bottom: -27vw;
+      right: 280px;
+      width: 100px;
+    }
+    @media (max-width: 575px) {
+      bottom: -32vw;
+      right: 270px;
+      width: 70px;
+    }
+    @media (max-width: 414px) {
+      bottom: -40vw;
+      right: 285px;
+    }
+    p {
+      color: #fff !important;
+      font-size: 14px;
+      margin-bottom: 0rem;
+    }
+    img {
+      width: 100px;
+      @media (max-width: 991px) {
+        width: 80px;
+      }
+      @media (max-width: 767px) {
+        width: 100px;
+      }
+    }
+  }
+  .line1,
+  .line2 {
+    position: relative;
+  }
+
+  .line1 {
+    @media (max-width: 767px) {
+      bottom: -360px;
+    }
+  }
+
+  .line2 {
+    @media (max-width: 991px) {
+      margin-left: 30px;
+    }
+    @media (max-width: 767px) {
+      bottom: -120px;
+    }
+  }
+  .ai_anchor_line1 {
+    max-width: 1500px;
+    position: absolute;
+    bottom: 0px;
+    left: 165px;
+    object-fit: cover;
+    @media (max-width: 1200px) {
+      max-width: 1000px;
+      left: 140px;
+    }
+    @media (max-width: 991px) {
+      width: 85vw;
+      left: 123px;
+    }
+    @media (max-width: 767px) {
+      left: 175px;
+      bottom: -138px;
+    }
+    @media (max-width: 575px) {
+      left: 168px;
+    }
+  }
+  .ai_anchor_line2 {
+    width: 1300px;
+    height: 290px;
+    position: absolute;
+    bottom: 6px;
+    left: -1238px;
+
+    @media (max-width: 1200px) {
+      width: 830px;
+      height: auto;
+      bottom: -5px;
+      left: -775px;
+    }
+
+    @media (max-width: 991px) {
+      width: 700px;
+      height: auto;
+      bottom: -5px;
+      left: -665px;
+    }
+    @media (max-width: 767px) {
+      width: 100vw;
+      bottom: -15vw;
+      left: -345px;
+    }
+    @media (max-width: 575px) {
+      left: -333px;
+    }
+  }
+  p {
+    font-weight: 900;
+    letter-spacing: 3px;
+    text-align: center;
+    margin-bottom: 0.3rem;
+    font-size: 22px;
+
+    @media (max-width: 991px) {
+      font-size: 20px;
+    }
+    @media (max-width: 767px) {
+      font-size: 16px;
+    }
+  }
+}
+.img-fluid {
+  max-width: 100%;
+  height: auto;
+}
+.progress-item {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+}
+</style>

+ 41 - 37
frontend/src/views/main/Start.vue

@@ -1,47 +1,51 @@
-
 <template>
   <router-view></router-view>
 </template>
-  
+
 <script lang="ts">
-  import { onBeforeRouteUpdate } from 'vue-router'
-  import type { RouteLocationNormalized , NavigationGuardNext} from 'vue-router';
-  import { useMainStore } from '@/stores/main';
-  import { storeToRefs } from "pinia";
-  export default {
-    setup(){
-      onBeforeRouteUpdate((to, from, next) =>  {
-        startRouteGuard(to, from, next);
-      });
-    },
-    beforeRouteEnter(to, from, next){
+import { onBeforeRouteUpdate } from "vue-router";
+import type { RouteLocationNormalized, NavigationGuardNext } from "vue-router";
+import { useMainStore } from "@/stores/main";
+import { storeToRefs } from "pinia";
+export default {
+  setup() {
+    onBeforeRouteUpdate((to, from, next) => {
       startRouteGuard(to, from, next);
-    }
+    });
+  },
+  beforeRouteEnter(to, from, next) {
+    startRouteGuard(to, from, next);
+  },
+};
 
-  } 
-  // variable
+//function
+const startRouteGuard = async (
+  to: RouteLocationNormalized,
+  from: RouteLocationNormalized,
+  next: NavigationGuardNext
+) => {
+  const mainStore = useMainStore();
+  const mainStoreRef = storeToRefs(mainStore);
 
-  // excute in setup
-  
-  //lifecycle
-  
-  //function
-  const startRouteGuard = async (to:RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
-    const mainStore = useMainStore();
-    const mainStoreRef = storeToRefs(mainStore);
+  if (to.path === "/qrcode") {
+    next();
+    mainStore.qrCheckLoggedIn();
+  } else {
     mainStore.checkLoggedIn();
-    if (mainStoreRef.readIsLoggedIn.value) {
-      if (to.path === '/login' || to.path === '/') {
-        next('/main/dashboard');
-      } else {
-        next();
-      }
-    } else if (mainStoreRef.readIsLoggedIn.value === false) {
-      if (to.path === '/' || (to.path as string).startsWith('/main')) {
-        next('/login');
-      } else {
-        next();
-      }
+  }
+
+  if (mainStoreRef.readIsLoggedIn.value) {
+    if (to.path === "/login" || to.path === "/") {
+      next("/main/dashboard");
+    } else {
+      next();
+    }
+  } else if (mainStoreRef.readIsLoggedIn.value === false) {
+    if (to.path === "/" || (to.path as string).startsWith("/main")) {
+      next("/login");
+    } else {
+      next();
     }
-  };
+  }
+};
 </script>