浏览代码

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

tomoya 2 年之前
父节点
当前提交
e93969cd22

+ 9 - 2
frontend/.env

@@ -1,5 +1,12 @@
-VITE_APP_DOMAIN_DEV=dev.cloud.choozmo.com
+# VITE_APP_DOMAIN_DEV=cloud.choozmo.com # 正式
+VITE_APP_DOMAIN_DEV=dev.ai-anchor.com # 測試
+# VITE_APP_DOMAIN_DEV=192.168.192.252 # 測試
+# VUE_APP_DOMAIN_DEV=local.dockertoolbox.tiangolo.com
+# VUE_APP_DOMAIN_DEV=localhost.tiangolo.com
+# VUE_APP_DOMAIN_DEV=dev.ai-anchor.com
 VITE_APP_DOMAIN_STAG=stag.ai-anchor.com
 VITE_APP_DOMAIN_PROD=cloud.choozmo.com
 VITE_APP_NAME=AI anchor
-VITE_APP_ENV=dev
+VITE_APP_ENV=production
+# VUE_APP_ENV=staging
+# VUE_APP_ENV=production

+ 8 - 9
frontend/src/api.ts

@@ -1,6 +1,6 @@
 import axios from "axios";
 import { apiUrl } from "@/env";
-import type { IUserProfile, IUserProfileUpdate, IUserProfileCreate, Video, VideoCreate, ArticleCreate, ImageDownload } from "@/interfaces";
+import type { IUserProfile, IUserProfileUpdate, IUserProfileCreate, Video, VideoCreate} from "@/interfaces";
 
 function authHeaders(token: string) {
   return {
@@ -49,15 +49,15 @@ export const api = {
   async registerUser(data: IUserProfileCreate) {
     return axios.post(`${apiUrl}/api/v1/users/open`, data);
   },
-  async testCeleryMsg(token: string, data: { msg: string }) {
-    return axios.post<{ msg: string }>(`${apiUrl}/api/v1/utils/test-celery/msg`, data, authHeaders(token));
+  async testCeleryMsg(token: string, data:{msg: string}){
+    return axios.post<{msg:string}>(`${apiUrl}/api/v1/utils/test-celery/msg`, data, authHeaders(token));
   },
-  async testCeleryFile(token: string, file: File) {
+  async testCeleryFile(token: string, file: File){
     const formData = new FormData();
     formData.append("file", file)
-    return axios.post<{ msg: string }>(`${apiUrl}/api/v1/utils/test-celery/file`, formData, authHeaders(token));
+    return axios.post<{msg:string}>(`${apiUrl}/api/v1/utils/test-celery/file`, formData, authHeaders(token));
   },
-  async uploadPlot(token: string, video_data: VideoCreate, file: File) {
+  async uploadPlot(token: string, video_data:VideoCreate, file: File){
     const formData = new FormData();
     formData.append("title", video_data.title)
     formData.append("anchor_id", video_data.anchor_id.toString())
@@ -82,11 +82,10 @@ export const api = {
         'Authorization': `Bearer ${token}`
       },
     }).then((response) => {
-      console.log('getImage response', response);
       const href = URL.createObjectURL(response.data);
       const link = document.createElement('a');
       link.href = href;
-      link.setAttribute('download', `${data.stored_file_name}_hr.png`); //or any other extension
+      link.setAttribute('download', `${data.file_name}_hr.png`); //or any other extension
       document.body.appendChild(link);
       link.click();
       document.body.removeChild(link);
@@ -109,5 +108,5 @@ export const api = {
     params.append("password", password);
 
     return axios.post(`${apiUrl}/api/v1/login/google/access-token`, params);
-  }
+  },
 };

二进制
frontend/src/assets/img/step/step-03.png


+ 4 - 0
frontend/src/env.ts

@@ -1,16 +1,20 @@
 const env = import.meta.env.VITE_APP_ENV;
 
 let envApiUrl = "";
+let envWsUrl = "";
 
 if (env === "production") {
   envApiUrl = `https://${import.meta.env.VITE_APP_DOMAIN_PROD}`;
+  envWsUrl = `wss://${import.meta.env.VITE_APP_DOMAIN_DEV}`;
 } else if (env === "staging") {
   envApiUrl = `https://${import.meta.env.VITE_APP_DOMAIN_STAG}`;
 } else {
   envApiUrl = `http://${import.meta.env.VITE_APP_DOMAIN_DEV}`;
+  envWsUrl = `ws://${import.meta.env.VITE_APP_DOMAIN_DEV}`;
 }
 
 export const apiUrl = envApiUrl;
+export const wsUrl = envWsUrl;
 export const appName = import.meta.env.VITE_APP_NAME;
 export const openRegisration = import.meta.env.VITE_APP_OPEN_REGISRATION;
 

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

@@ -9,19 +9,19 @@ export interface IUserProfile {
 }
 
 export interface IUserProfileUpdate {
-  email?: string;
-  full_name?: string;
-  password?: string;
-  is_active?: boolean;
-  is_superuser?: boolean;
+    email?: string;
+    full_name?: string;
+    password?: string;
+    is_active?: boolean;
+    is_superuser?: boolean;
 }
 
 export interface IUserProfileCreate {
-  email: string;
-  full_name?: string;
-  password: string;
-  is_active?: boolean;
-  is_superuser?: boolean;
+    email: string;
+    full_name?: string;
+    password: string;
+    is_active?: boolean;
+    is_superuser?: boolean;
 }
 
 export interface AppNotification {
@@ -39,7 +39,6 @@ export interface MainState {
   dashboardShowDrawer: boolean;
   notifications: AppNotification[];
   videos: Video[];
-  images: Image[];
 };
 
 export interface Video {
@@ -49,7 +48,7 @@ export interface Video {
   progress_state: string;
 }
 
-export interface VideoCreate {
+export interface VideoCreate{
   title: string;
   anchor_id: number;
   lang_id: number;
@@ -65,9 +64,10 @@ export interface Image {
   file_name: string;
   stored_file_name: string;
   content: string;
+  state: string;
 }
 
 export interface ImageDownload {
   file_name: string;
   stored_file_name: string;
-}
+}

+ 3 - 1
frontend/src/main.ts

@@ -14,6 +14,8 @@ app.use(vuetify);
 app.use(i18n);
 app.use(vue3GoogleLogin, {
     clientId: '136107811725-n71808u8t465f1afhpe2e5j7mn606nd8.apps.googleusercontent.com'
-})
+});
 
 app.mount("#app");
+
+export default app;

+ 26 - 7
frontend/src/stores/main.ts

@@ -4,9 +4,9 @@ import { api } from "@/api"
 import router from "@/router"
 import { getLocalToken, removeLocalToken, saveLocalToken } from "@/utils";
 import type { AppNotification } from '@/interfaces';
-import type { IUserProfile, IUserProfileCreate, IUserProfileUpdate, MainState, Video, VideoCreate, ArticleCreate, Image, ImageDownload } from '@/interfaces';
+import type { IUserProfile, IUserProfileCreate, IUserProfileUpdate, MainState, Video, VideoCreate } from '@/interfaces';
 import i18n from '@/plugins/i18n'
-import { GoogleLogin } from "vue3-google-login";
+import WS from "@/stores/ws";
 
 const defaultState: MainState = {
   isLoggedIn: null,
@@ -17,7 +17,6 @@ const defaultState: MainState = {
   dashboardShowDrawer: true,
   notifications: [],
   videos: [],
-  images: [],
 };
 
 export const useMainStore = defineStore("MainStoreId", {
@@ -272,12 +271,12 @@ export const useMainStore = defineStore("MainStoreId", {
               const tmpImage: Image = {
                 file_name: file[i].name,
                 stored_file_name: element,
-                content: "sr"
+                content: "sr",
+                state: "subscribe"
               };
               this.addImage(tmpImage);
             }
-            localStorage.setItem('imagesList',JSON.stringify(this.images));
-            console.log('this.images',this.images);
+            // localStorage.setItem('imagesList', JSON.stringify(this.images));
           })
         );
         mainStore.removeNotification(loadingNotification);
@@ -291,7 +290,6 @@ export const useMainStore = defineStore("MainStoreId", {
       }
     },
     async getImage(data: ImageDownload) {
-      console.log('getImage data', data);
       const mainStore = useMainStore();
       try {
         const response = (
@@ -307,6 +305,27 @@ export const useMainStore = defineStore("MainStoreId", {
     addImage(payload: Image) {
       this.images.push(payload);
     },
+    finishImage(payload: string) {
+      let image = this.images.filter(e => {
+        return e.stored_file_name === payload
+      });
+
+      if (image) {
+        image.map(e => {
+          e.state = "completed";
+        })
+      }
+
+      // 全部完成後關閉 WebSocket
+      // let processing = this.images.find(e => e.state !== "completed")
+      // if (!processing) {
+      //   setTimeout(() => {
+      //     WS.close();
+      //   }, 1000)
+      // }
+
+      return !this.images.some(e => e.state === "completed")
+    },
     async uploadArticle(article_data: ArticleCreate) {
       const mainStore = useMainStore();
       try {

+ 5 - 0
frontend/src/stores/ws.ts

@@ -0,0 +1,5 @@
+import { wsUrl } from "@/env";
+
+const WS = new WebSocket(`${wsUrl}/api/v1/images/sr`);
+
+export default WS;

+ 2 - 2
frontend/src/views/Login.vue

@@ -15,7 +15,7 @@ const callback: CallbackTypes.CredentialCallback = (response: any) => {
   console.log("Handle the response", response);
   const userData: any = decodeCredential(response.credential);
   console.log("Handle the userData", userData);
-  mainStore.googleLogin(userData.email, "google");
+  mainStore.googleLogin(userData.email);
 };
 
 // const data = ref();
@@ -137,7 +137,7 @@ onMounted(() => {
               <span></span>
             </section>
 
-            <div class="mx-auto mt-5" style="max-width: 235px">
+            <div class="w-100 mx-auto mt-5" style="max-width: 280px">
               <GoogleLogin
                 :callback="callback"
                 prompt

+ 29 - 40
frontend/src/views/main/Article.vue

@@ -1,42 +1,23 @@
 <script setup lang="ts">
-import { ref, reactive } from "vue";
+import { ref } from "vue";
 import { useMainStore } from "@/stores/main";
 import { required } from "@/utils";
 import { useI18n } from "vue-i18n";
-import type { ArticleCreate } from "@/interfaces";
-// import Dialog from "@/components/Dialog.vue";
 
 const mainStore = useMainStore();
 const { t } = useI18n();
 const valid = ref(true);
+const dialog = ref(false);
+const title = ref("");
 const Form = ref();
-let title = ref("");
-let link = ref("");
-let content = ref("");
-
-// props
-// let dialog = reactive({
-//   msg: "上傳成功!",
-//   state: "success",
-//   show: false,
-// });
 
 async function Submit() {
-  // setTimeout(() => {
-  //   dialog.show = true;
-  // }, 2000);
+  setTimeout(() => {
+    dialog.value = true;
+  }, 2000);
   await (Form as any).value.validate();
   if (valid.value) {
     valid.value = false;
-
-    const article_data: ArticleCreate = {
-      title: title.value,
-      link: link.value,
-      content: content.value,
-    };
-
-    await mainStore.uploadArticle(article_data);
-    // (Form as any).value.reset();
   }
 }
 </script>
@@ -56,14 +37,9 @@ async function Submit() {
             prepend-icon="title"
           >
           </v-text-field>
-          <v-text-field
-            v-model="link"
-            :label="$t('articleLink')"
-            prepend-icon="link"
-          >
+          <v-text-field :label="$t('articleLink')" prepend-icon="link">
           </v-text-field>
           <v-textarea
-            v-model="content"
             :label="$t('articleContent')"
             prepend-icon="edit_document"
           ></v-textarea>
@@ -77,17 +53,30 @@ async function Submit() {
       </v-card-actions>
     </v-card>
 
-    <!-- <template>
+    <template>
       <div class="text-center">
-        <Dialog
-          :msg="dialog.msg"
-          :state="dialog.state"
-          :dialog="dialog.show"
-          @close="dialog.show = false"
-        ></Dialog>
+        <v-dialog v-model="dialog" width="auto">
+          <v-card>
+            <v-card-text>
+              <section class="d-flex flex-column align-center">
+                <v-icon
+                  style="font-size: 70px"
+                  icon="info"
+                  color="orange-darken-3"
+                />
+                <p class="mt-3">文章處理需要約 5-10 分鐘,敬請耐心等候</p>
+              </section>
+            </v-card-text>
+            <v-card-actions>
+              <v-btn color="primary" block @click="dialog = false">
+              {{ t("close") }}</v-btn>
+            </v-card-actions>
+          </v-card>
+        </v-dialog>
       </div>
-    </template> -->
+    </template>
   </v-container>
 </template>
 
-<style lang="scss"></style>
+<style lang="scss">
+</style>

+ 60 - 63
frontend/src/views/main/Image.vue

@@ -1,18 +1,38 @@
 <script setup lang="ts">
-import { ref, reactive, onMounted } from "vue";
+import { ref, reactive } from "vue";
 import { useMainStore } from "@/stores/main";
 import { required } from "@/utils";
 import { useI18n } from "vue-i18n";
 import type { ImageDownload } from "@/interfaces";
 import Dialog from "@/components/Dialog.vue";
+import WS from "@/stores/ws";
 
 const mainStore = useMainStore();
 const { t } = useI18n();
 const valid = ref(true);
+const dialog = ref(false);
+const title = ref("");
 const Form = ref();
 let imgFiles = ref();
 let imgList: any[] = reactive([]);
 
+async function upload() {
+  for (let i = 0; i < imgFiles.value.files.length; i++) {
+    const item = imgFiles.value.files[i];
+    imgList.push(item);
+    console.log("element", item.name);
+  }
+}
+
+// async function Submit() {
+//   setTimeout(() => {
+//     dialog.value = true;
+//   }, 2000);
+//   await (Form as any).value.validate();
+//   if (valid.value) {
+//     valid.value = false;
+//   }
+// }
 // props
 let dialog = reactive({
   msg: "圖片處理需要幾秒鐘的時間,敬請耐心等候",
@@ -21,6 +41,8 @@ let dialog = reactive({
 });
 
 async function Submit() {
+  WS.send("subscribe");
+
   setTimeout(() => {
     dialog.show = true;
   }, 2000);
@@ -28,27 +50,36 @@ async function Submit() {
   (Form as any).value.reset();
   for (let i = 0; i < mainStore.images.length; i++) {
     const element = mainStore.images[i];
+    imgList = imgList.filter(
+      (e) => e.stored_file_name === element.stored_file_name
+    );
     imgList.push(element);
   }
 }
 
 onMounted(() => {
-  if (imgList.length === 0) {
-    let images: any | null = localStorage.getItem("imagesList");
-    if (images) {
-      images = JSON.parse(images);
-      for (let i = 0; i < images.length; i++) {
-        const item = images[i];
-        imgList.push(item);
-      }
-    }
-  }
+  // 存入 localStorage
+  // if (imgList.length === 0) {
+  //   let images: any | null = localStorage.getItem("imagesList");
+  //   if (images) {
+  //     images = JSON.parse(images);
+  //     for (let i = 0; i < images.length; i++) {
+  //       const item = images[i];
+  //       imgList.push(item);
+  //     }
+  //   }
+  // }
+
+  // webSocket
+  WS.onmessage = function (e) {
+    mainStore.finishImage(e.data);
+  };
 });
 
 async function downloadImg(name: string, id: string) {
   const data: ImageDownload = {
-    file_name: id,
-    stored_file_name: name.split(".")[0],
+    file_name: name.split(".")[0],
+    stored_file_name: id,
   };
 
   await mainStore.getImage(data);
@@ -58,18 +89,18 @@ const headers = [
   {
     title: "檔名",
     sortable: true,
-    key: "file_name",
+    key: "name",
     align: "left",
   },
   {
     title: t("state"),
     sortable: true,
-    key: "progress_state",
+    key: "state",
     align: "left",
   },
   {
-    title: t("download"),
-    key: "stored_file_name",
+    title: t("preview"),
+    key: "id",
   },
 ];
 </script>
@@ -81,29 +112,16 @@ const headers = [
         <h3 class="card-title mb-3">圖片優化</h3>
       </v-card-title>
       <v-card-text>
-        <!-- <section class="d-flex flex-column form-title">
-        <img src="@/assets/img/icon/add-image.png" alt="" class="mb-4" />
-        <p>請點擊加入圖片並開始優化</p>
-       </section> -->
-
-        <v-form v-model="valid" ref="Form">
-          <!-- <img src="@/assets/img/icon/add-image.png" alt="" class="mb-4" /> -->
-          <!-- <input
+        <v-form v-model="valid" ref="Form" class="img-form">
+          <img src="@/assets/img/icon/add-image.png" alt="" class="mb-4" />
+          <input
             @change="upload()"
             ref="imgFiles"
             type="file"
             multiple
             class="file-input"
-          /> -->
-
-          <v-file-input
-            v-model="imgFiles"
-            multiple
-            label="請選擇圖片"
-            prepend-icon="add_photo_alternate"
-          ></v-file-input>
-
-          <!-- <p>請點擊加入圖片並開始優化</p> -->
+          />
+          <p>請點擊加入圖片並開始優化</p>
         </v-form>
       </v-card-text>
       <v-card-actions class="justify-center">
@@ -113,8 +131,7 @@ const headers = [
           color="primary"
           class="px-5"
           prepend-icon="file_upload"
-          @click="Submit()"
-          >上傳</v-btn
+          >上傳圖片</v-btn
         >
       </v-card-actions>
     </v-card>
@@ -124,17 +141,13 @@ const headers = [
         <h3 class="card-title mb-3">上傳清單</h3>
       </v-card-title>
 
-      <v-data-table :headers="headers" :items="imgList">
-        <template v-slot:item.progress_state="{ item }">
-          <span v-if="item.raw.progress_state === 'completed'">
+      <v-data-table :headers="headers" :items="mainStore.images">
+        <template v-slot:item.state="{ item }">
+          <span v-if="item.raw.state === 'completed'">
             <v-icon icon="check_circle" color="success" />
             完成
           </span>
-          <span v-else-if="item.raw.progress_state === 'waiting'">
-            <v-icon icon="pending" color="warning" />
-            等待中
-          </span>
-          <span v-else-if="item.raw.progress_state === 'processing'">
+          <span v-else>
             <v-progress-circular
               indeterminate
               color="info"
@@ -142,28 +155,12 @@ const headers = [
             ></v-progress-circular>
             處理中
           </span>
-          <span v-else>
-            <v-icon icon="check_circle" color="success" />
-            完成
-          </span>
         </template>
-        <template v-slot:item.stored_file_name="{ item }">
-          <v-btn
-            flat
-            @click="downloadImg(item.raw.file_name, item.raw.stored_file_name)"
-          >
-            <v-icon icon="crop_original" />
-          </v-btn>
+        <template v-slot:item.id="{ item }">
+          <v-icon icon="crop_original" />
         </template>
       </v-data-table>
     </v-card>
-
-    <Dialog
-      :msg="dialog.msg"
-      :state="dialog.state"
-      :dialog="dialog.show"
-      @close="dialog.show = false"
-    ></Dialog>
   </v-container>
 </template>
 

+ 50 - 87
frontend/src/views/main/Upload.vue

@@ -3,19 +3,28 @@ import { ref, reactive, watch, computed } from "vue";
 import { useMainStore } from "@/stores/main";
 import { required } from "@/utils";
 import { useI18n } from "vue-i18n";
+import { useDisplay } from "vuetify";
 import type { VideoCreate } from "@/interfaces";
 import router from "@/router";
-import Dialog from "@/components/Dialog.vue";
 
-// props
-let dialog = reactive({
-  msg: "影片處理需要約 5-10 分鐘,敬請耐心等候",
-  state: "info",
-  show: false,
+const { t } = useI18n();
+const { name } = useDisplay();
+const width = computed(() => {
+  switch (name.value) {
+    case "xs":
+      return 6;
+    case "sm":
+      return 4;
+    case "md":
+      return 3;
+    case "lg":
+      return 2;
+  }
+  return "auto";
 });
 
-const { t } = useI18n();
 const valid = ref(true);
+const dialog = ref(false);
 const title = ref("");
 const zipFiles = ref();
 const Form = ref();
@@ -26,102 +35,102 @@ const anchorList = reactive([
   {
     anchor_id: 0,
     language_id: 1,
-    name: "Angela",
+    name: "Peggy",
   },
   {
     anchor_id: 1,
     language_id: 1,
-    name: "半身主播-1",
+    name: "Jocelyn",
   },
   {
     anchor_id: 2,
     language_id: 1,
-    name: "半身主播-2",
+    name: "Summer",
   },
   {
     anchor_id: 3,
     language_id: 1,
-    name: "半身主播-3",
+    name: "Angela",
   },
   {
     anchor_id: 4,
     language_id: 1,
-    name: "半身主播-4",
+    name: "半身主播-1",
   },
   {
     anchor_id: 5,
     language_id: 1,
-    name: "半身主播-5",
+    name: "半身主播-2",
   },
   {
     anchor_id: 6,
     language_id: 1,
-    name: "半身主播-6",
+    name: "半身主播-3",
   },
   {
     anchor_id: 7,
     language_id: 1,
-    name: "半身主播-7",
+    name: "半身主播-4",
   },
   {
     anchor_id: 8,
     language_id: 1,
-    name: "半身主播-8",
+    name: "半身主播-5",
   },
   {
     anchor_id: 9,
     language_id: 1,
-    name: "半身主播-9",
+    name: "半身主播-6",
   },
   {
     anchor_id: 10,
     language_id: 1,
-    name: "半身主播-10",
+    name: "半身主播-7",
   },
   {
     anchor_id: 11,
     language_id: 1,
-    name: "半身主播-11",
+    name: "半身主播-8",
   },
   {
     anchor_id: 12,
     language_id: 1,
-    name: "半身主播-12",
+    name: "半身主播-9",
   },
   {
     anchor_id: 13,
     language_id: 1,
-    name: "半身主播-13",
+    name: "半身主播-10",
   },
   {
     anchor_id: 14,
     language_id: 1,
-    name: "半身主播-14",
+    name: "半身主播-11",
   },
   {
     anchor_id: 15,
     language_id: 1,
-    name: "半身主播-15",
+    name: "半身主播-12",
   },
   {
     anchor_id: 16,
     language_id: 1,
-    name: "半身主播-16",
+    name: "半身主播-13",
   },
   {
     anchor_id: 17,
     language_id: 1,
-    name: "Peggy",
+    name: "半身主播-14",
   },
   {
     anchor_id: 18,
     language_id: 1,
-    name: "Jocelyn",
+    name: "半身主播-15",
   },
   {
     anchor_id: 19,
     language_id: 1,
-    name: "Summer",
+    name: "半身主播-16",
   },
 ]);
 
@@ -160,23 +169,20 @@ let items = reactive([
 
 // 取得圖片路徑
 const getImageUrl = (imgFolder: string, name: string) => {
-  return new URL(`../../assets/img/${imgFolder}/${name}.webp`, import.meta.url)
-    .href;
+  return new URL(`../../assets/img/${imgFolder}/${name}.webp`, import.meta.url).href;
 };
 
 const mainStore = useMainStore();
 
 watch(dialog, (newVal, oldVal) => {
-  if (!newVal.show) {
-    setTimeout(() => {
-      router.push("/main/progress");
-    }, 500);
+  if (!newVal) {
+    router.push("/main/progress");
   }
 });
 
 async function Submit() {
   setTimeout(() => {
-    dialog.show = true;
+    dialog.value = true;
   }, 2000);
   await (Form as any).value.validate();
   if (valid.value) {
@@ -241,22 +247,9 @@ async function Submit() {
                           dark
                           @click="toggle"
                           :title="n.name"
-                          :disabled="n.anchor_id !== 0"
                         >
                           <v-scroll-y-transition>
-                            <div v-if="n.anchor_id !== 0" class="img-disabled">
-                              <img
-                                :src="getImageUrl('anchor', n.name)"
-                                alt=""
-                              />
-                              <p>Coming Soon</p>
-                            </div>
-
-                            <img
-                              v-else
-                              :src="getImageUrl('anchor', n.name)"
-                              alt=""
-                            />
+                            <img :src="getImageUrl('anchor', n.name)" alt="" />
                           </v-scroll-y-transition>
                         </v-card>
                       </v-item>
@@ -281,7 +274,7 @@ async function Submit() {
                     v-for="n in templateList"
                     :key="n.template_id"
                     v-slot="{ isSelected, toggle, selectedClass }"
-                    disabled
+                    :disabled="n.template_id !== 0"
                   >
                     <v-card
                       color="grey-lighten-1"
@@ -294,9 +287,10 @@ async function Submit() {
                       >
                         <v-icon icon="done" color="white" />
                       </span>
-                      <div class="img-disabled">
+                      <img :src="getImageUrl('template', n.img)" alt="" />
+                      <div :class="{ 'img-disabled': n.template_id !== 0 }">
                         <img :src="getImageUrl('template', n.img)" alt="" />
-                        <p>Coming Soon</p>
+                        <p v-if="n.template_id !== 0">Coming Soon</p>
                       </div>
                       <!-- <img :src="getImageUrl('template', n.img)" alt="" /> -->
                     </v-card>
@@ -352,8 +346,9 @@ async function Submit() {
                 <br />
                 (建議 10 個字內,若超過請使用換行符號)
               </li>
-              <li>2. 大標字數勿超過中文 15 字、英文 30 字</li>
-              <li>3. 音檔請留空白</li>
+              <li>2. 字幕段落勿超過中文 25 字、英文 50 字</li>
+              <li>3. 大標字數勿超過中文 15 字、英文 30 字</li>
+              <li>4. 音檔請留空白</li>
             </ul>
             <p class="mt-5 excerpt">以下為顯示效果:</p>
             <img src="@/assets/img/step/step-04.png" alt="" />
@@ -377,7 +372,7 @@ async function Submit() {
 
     <template>
       <div class="text-center">
-        <!-- <v-dialog v-model="dialog" width="auto">
+        <v-dialog v-model="dialog" width="auto">
           <v-card>
             <v-card-text>
               <section class="d-flex flex-column align-center">
@@ -395,14 +390,7 @@ async function Submit() {
               }}</v-btn>
             </v-card-actions>
           </v-card>
-        </v-dialog> -->
-
-        <Dialog
-          :msg="dialog.msg"
-          :state="dialog.state"
-          :dialog="dialog.show"
-          @close="dialog.show = false"
-        ></Dialog>
+        </v-dialog>
       </div>
     </template>
   </v-container>
@@ -534,29 +522,4 @@ async function Submit() {
     margin-bottom: 2px;
   }
 }
-
-.img-disabled {
-  position: relative;
-  z-index: 999;
-  background-color: #ccc;
-  margin-bottom: -5px;
-  img {
-    opacity: 0.7;
-  }
-  p {
-    position: absolute;
-    top: 50%;
-    left: 50%;
-    color: #fff;
-    transform: translate(-50%, -50%);
-    font-size: 16px;
-    text-align: center;
-    letter-spacing: 1px;
-    text-shadow: 2px 2px 6px #000;
-  }
-}
-
-.v-card--disabled > :not(.v-card__loader) {
-  opacity: 1 !important;
-}
 </style>