2 コミット 8586e53ebe ... d9f4752ad8

作者 SHA1 メッセージ 日付
  SyuanYu d9f4752ad8 add icon 3 ヶ月 前
  SyuanYu 3dc1149d15 add zip_translate 3 ヶ月 前

+ 10 - 4
frontend/src/api.ts

@@ -198,15 +198,21 @@ export const api = {
     return axios.post<string>(`${apiUrl}/api/v1/payment/simpley-ecpay-payment?lang=${lang}`, payment_data);
   },
   async uploadVideoContent(content: string, file: []) {
-    // let content = "";
-    console.log('api content', content);
-    console.log('api file', file);
-
     const formData = new FormData();
+
     for (let index = 0; index < file.length; index++) {
       const element = file[index];
       formData.append("input_model", element)
     }
+
     return axios.post(`${apiUrl}/api/v1/videos/phone_input_json?text=${content}`, formData);
   },
+  async zipTranslate(lang: string, file: File) {
+    const formData = new FormData();
+    formData.append("upload_file", file);
+
+    return axios.post<string>(`${apiUrl}/api/v1/text2zip/zip-translate?lang=${lang}`, formData, {
+      responseType: 'blob' // 指定回傳類型為 Blob
+    });
+  },
 };

+ 3 - 1
frontend/src/language/en.json

@@ -78,5 +78,7 @@
     "autoCreateVideoMaterials": "Auto Create Video Materials",
     "generateZip": "Generate ZIP",
     "paragraph": "Paragraph",
-    "addParagraph": "Add Paragraph"
+    "addParagraph": "Add Paragraph",
+    "translate": "Translate",
+    "zipTranslate": "ZIP Translate"
 }

+ 3 - 1
frontend/src/language/zh.json

@@ -78,5 +78,7 @@
     "autoCreateVideoMaterials": "自動製作影片素材",
     "generateZip": "生成 ZIP",
     "paragraph": "段落",
-    "addParagraph": "新增段落"
+    "addParagraph": "新增段落",
+    "translate": "翻譯",
+    "zipTranslate": "ZIP 翻譯"
 }

+ 5 - 0
frontend/src/router/index.ts

@@ -138,6 +138,11 @@ const router = createRouter({
               name: 'AiArticle',
               component: () => import('@/views/main/AiArticle.vue'),
             },
+            {
+              path: 'zip-translate',
+              name: 'zip-translate',
+              component: () => import('@/views/main/ZipTranslate.vue'),
+            },
             {
               path: 'profile',
               name: 'profile',

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

@@ -626,14 +626,7 @@ export const useMainStore = defineStore("MainStoreId", {
       }
     },
     async uploadVideoContent(content: VideoContent, file: []) {
-      console.log('uploadVideoContent');
-
-      console.log('content', content);
-      console.log('file', file);
-
       let jsonContent = JSON.stringify(content);
-      console.log('jsonContent', jsonContent);
-
 
       const mainStore = useMainStore();
       try {
@@ -649,5 +642,22 @@ export const useMainStore = defineStore("MainStoreId", {
         // await mainStore.checkApiError(error);
       }
     },
+    async zipTranslate(lang: string, file: File) {
+      const mainStore = useMainStore();
+      try {
+        const response = (
+          await Promise.all([
+            api.zipTranslate(lang, file),
+            await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 0)),
+          ]).then(data => {
+            return data;
+          })
+        );
+
+        return response;
+      } catch (error) {
+        await mainStore.checkApiError(error);
+      }
+    },
   }
 });

+ 3 - 258
frontend/src/views/main/GenerateZip.vue

@@ -1,73 +1,13 @@
 <script setup lang="ts">
-import { ref, reactive, watch, computed } from "vue";
+import { ref, reactive } from "vue";
 import { useMainStore } from "@/stores/main";
 import { required } from "@/utils";
 import { useI18n } from "vue-i18n";
-import { wsUrl } from "@/env";
-import type { VideoCreate } from "@/interfaces";
-import type { VideoUploaded } from "@/interfaces";
-import router from "@/router";
-import Dialog from "@/components/Dialog.vue";
 
 const { t } = useI18n();
 const mainStore = useMainStore();
-const WS = mainStore.videosWebSocket;
 const valid = ref(true);
-const title = ref("");
-const zipFiles = ref();
 const Form = ref();
-let selectAnchor = ref("angela");
-let selectTemplate = ref("style1");
-
-// props
-let dialog = reactive({
-  msg: "",
-  state: "info",
-  show: false,
-});
-
-watch(dialog, (newVal) => {
-  if (!newVal.show && newVal.state === "error") {
-    return;
-  } else if (!newVal.show && newVal.state === "success") {
-    router.push("/main/progress");
-  }
-});
-
-async function Submit() {
-  WS.send("subscribe");
-  await (Form as any).value.validate();
-  if (valid.value) {
-    valid.value = false;
-
-    const video_data: VideoCreate = {
-      title: title.value,
-      anchor: selectAnchor.value,
-      style: selectTemplate.value,
-      lang: "zh",
-    };
-
-    const ret: VideoUploaded = await mainStore.uploadPlot(
-      video_data,
-      zipFiles.value[0]
-    );
-
-    if (ret.accepted) {
-      dialog.msg = t("acceptZipMessage");
-      dialog.state = "success";
-      dialog.show = true;
-    } else {
-      dialog.msg = ret.error_message!;
-      dialog.state = "error";
-      dialog.show = true;
-    }
-
-    valid.value = true;
-
-    // (Form as any).value.reset();
-  }
-}
-
 let paragraphList = reactive([""]);
 
 // 新增段落
@@ -82,14 +22,11 @@ const zipFile = ref(); // 儲存 ZIP 檔案
 async function generateZip() {
   zipFile.value = null;
   loading.value = true;
-  const response: any = await mainStore.generateZip(model.value, paragraphList);
 
+  const response: any = await mainStore.generateZip(model.value, paragraphList);
   zipFile.value = new Blob([response[0].data], { type: "application/zip" });
 
   loading.value = false;
-
-  console.log("response", response);
-  console.log("zipFile.value", zipFile.value);
 }
 
 // ZIP 檔案下載
@@ -194,196 +131,4 @@ function downloadZipFile() {
   </v-container>
 </template>
 
-<style lang="scss">
-.anchor-list {
-  ul {
-    display: grid;
-    grid-template-columns: repeat(auto-fit, minmax(185px, max-content));
-    grid-gap: 20px;
-    justify-content: center;
-    padding: initial;
-    li {
-      list-style-type: none;
-    }
-  }
-  img {
-    width: 190px;
-    height: 155px;
-    object-fit: cover;
-  }
-
-  .v-card--variant-elevated {
-    box-shadow: 0px 2px 5px 1px
-        var(--v-shadow-key-umbra-opacity, rgba(0, 0, 0, 0.2)),
-      0px 1px 1px 0px var(--v-shadow-key-penumbra-opacity, rgba(0, 0, 0, 0.14)),
-      0px 1px 3px 0px var(--v-shadow-key-penumbra-opacity, rgba(0, 0, 0, 0.12));
-  }
-
-  .v-card-item {
-    padding: 0;
-    text-align: center;
-    .v-card-title {
-      font-size: 18px;
-    }
-  }
-  .bg-success {
-    background: linear-gradient(
-      -225deg,
-      rgb(234, 84, 19) 35%,
-      rgb(178, 69, 146) 100%
-    ) !important;
-  }
-  .v-expansion-panel-text__wrapper {
-    padding: 0 !important;
-  }
-}
-
-.anchor-list,
-.template-list {
-  padding-left: 40px;
-  .v-expansion-panel-title {
-    height: 55px;
-    min-height: 0;
-  }
-}
-
-.template-list {
-  img {
-    width: 100%;
-    height: 180px;
-  }
-  .choose-btn {
-    padding: 5px;
-    position: absolute;
-    right: 8px;
-    bottom: 13px;
-    background: #ccc;
-    border-radius: 100px;
-  }
-  .active-color {
-    background: #ea5413;
-  }
-}
-
-.step-list {
-  list-style: none;
-  img {
-    width: 100%;
-    max-width: 1000px;
-  }
-  li {
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    font-size: 16px;
-    p {
-      line-height: 32px;
-    }
-    h4 {
-      margin: 20px auto;
-      color: #ea5413;
-      font-weight: bold;
-      text-align: center;
-      line-height: 34px;
-      font-size: 20px;
-    }
-  }
-
-  .link-btn {
-    display: inline-block;
-    padding: 12px 20px;
-    margin-top: 25px;
-    border-radius: 100px;
-    text-decoration: none;
-    color: #fff;
-    background: #ea5413;
-    transition: all 0.3s;
-    &:hover {
-      opacity: 0.8;
-    }
-  }
-
-  .point-list {
-    display: flex;
-    flex-direction: column;
-    align-items: baseline;
-    margin-left: 40px;
-  }
-
-  .point-content {
-    .base,
-    .advanced {
-      padding: 40px;
-      margin-top: 50px;
-      max-width: 1000px;
-      letter-spacing: 1px;
-      border-radius: 5px;
-    }
-
-    .base {
-      border: 4px solid #ea5413;
-    }
-
-    .advanced {
-      border: 4px dashed #ea5413;
-    }
-
-    ul {
-      display: flex;
-      flex-direction: column;
-      align-items: flex-start;
-
-      li {
-        margin: 5px 0;
-      }
-    }
-
-    h5 {
-      margin-bottom: 20px;
-      text-align: center;
-      font-size: 1.25rem;
-    }
-
-    hr {
-      margin: 30px;
-      border-color: #f2f2f2;
-      opacity: 0.3;
-    }
-  }
-
-  .excerpt::before {
-    content: "";
-    font-weight: bold;
-    display: inline-block;
-    border: 5px solid #ea5413;
-    border-radius: 20px;
-    margin-right: 10px;
-    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>
+<style lang="scss"></style>

+ 7 - 5
frontend/src/views/main/Main.vue

@@ -112,7 +112,9 @@ const routeGuardAdmin = async (
               <v-list-item-title>{{ t("progress") }}</v-list-item-title>
             </v-list-item>
             <v-list-item to="/main/generate-zip" prepend-icon="video_file">
-              <v-list-item-title>{{ t("autoCreateVideoMaterials") }}</v-list-item-title>
+              <v-list-item-title>{{
+                t("autoCreateVideoMaterials")
+              }}</v-list-item-title>
             </v-list-item>
             <!-- <v-list-item to="/main/make-image" prepend-icon="image">
               <v-list-item-title>圖片優化</v-list-item-title>
@@ -150,6 +152,9 @@ const routeGuardAdmin = async (
             <v-list-item to="/main/ai-article" prepend-icon="article">
               <v-list-item-title>AI 文章生成</v-list-item-title>
             </v-list-item>
+            <v-list-item to="/main/zip-translate" prepend-icon="folder_zip">
+              <v-list-item-title>{{ t("zipTranslate") }}</v-list-item-title>
+            </v-list-item>
           </v-list>
         </v-sheet>
         <!-- <v-divider></v-divider> -->
@@ -182,10 +187,7 @@ const routeGuardAdmin = async (
             >
               <v-list-item-title>Test Style Preview</v-list-item-title>
             </v-list-item>
-            <v-list-item
-              to="/main/admin/test-payment"
-              prepend-icon="payments"
-            >
+            <v-list-item to="/main/admin/test-payment" prepend-icon="payments">
               <v-list-item-title>Test Payment</v-list-item-title>
             </v-list-item>
           </v-list>

+ 2 - 2
frontend/src/views/main/Upload.vue

@@ -279,7 +279,7 @@ watch(templateId, (newVal) => {
   selectTemplate.value = templateList[newVal].template_id;
 });
 
-async function Submit() {
+async function submit() {
   WS.send("subscribe");
   await (Form as any).value.validate();
   if (valid.value) {
@@ -428,7 +428,7 @@ async function Submit() {
       </v-card-text>
       <v-card-actions>
         <v-spacer></v-spacer>
-        <v-btn @click="Submit" :disabled="!valid" variant="outlined">
+        <v-btn @click="submit" :disabled="!valid" variant="outlined">
           {{ t("send") }}
         </v-btn>
       </v-card-actions>

+ 133 - 0
frontend/src/views/main/ZipTranslate.vue

@@ -0,0 +1,133 @@
+<script setup lang="ts">
+import { ref, reactive, watch, computed } from "vue";
+import { useMainStore } from "@/stores/main";
+import { useI18n } from "vue-i18n";
+
+const { t } = useI18n();
+const mainStore = useMainStore();
+const valid = ref(true);
+const file = ref();
+const Form = ref();
+
+let langOptions = reactive([
+  {
+    title: "中文",
+    id: "zh-TW",
+  },
+  {
+    title: "英文",
+    id: "en",
+  },
+  {
+    title: "印尼",
+    id: "id",
+  },
+  {
+    title: "越南",
+    id: "vi",
+  },
+]);
+
+let lang = ref("");
+let loading = ref(false);
+const zipFile = ref(); // 儲存 ZIP 檔案
+
+async function submit() {
+  console.log("lang", lang.value);
+  console.log("file", file.value[0]);
+
+  zipFile.value = null;
+  loading.value = true;
+  const response: any = await mainStore.zipTranslate(lang.value, file.value[0]);
+
+  zipFile.value = new Blob([response[0].data], { type: "application/zip" });
+
+  loading.value = false;
+
+  console.log("response", response);
+  console.log("zipFile.value", zipFile.value);
+}
+
+// ZIP 檔案下載
+function downloadZipFile() {
+  if (!zipFile.value) {
+    console.error("沒有 ZIP 檔案可下載");
+    return;
+  }
+
+  // 生成 Blob URL
+  const url = URL.createObjectURL(zipFile.value);
+
+  const link = document.createElement("a");
+  link.href = url;
+  link.download = "影片素材.zip"; // 設定下載檔名
+  document.body.appendChild(link);
+
+  link.click();
+  document.body.removeChild(link);
+
+  // 釋放 Blob URL
+  URL.revokeObjectURL(url);
+}
+</script>
+
+<template>
+  <v-container fluid>
+    <v-card class="ma-3 pa-3">
+      <v-card-title primary-title>
+        <h3 class="card-title mb-3">{{ t("zipTranslate") }}</h3>
+      </v-card-title>
+      <v-card-text>
+        <v-form v-model="valid" ref="Form" class="d-flex">
+          <v-file-input
+            v-model="file"
+            :rules="[(v) => v.length || 'select zip file.']"
+            accept=".zip"
+            :label="$t('fileInput')"
+            prepend-icon="folder_zip"
+          ></v-file-input>
+
+          <v-select
+            v-model="lang"
+            label="語言"
+            :items="langOptions"
+            item-title="title"
+            item-value="id"
+            variant="solo"
+            class="mx-5 lang-list"
+            style="width: 100px"
+            :rules="[(v) => v.length || '尚未選擇語言']"
+          ></v-select>
+        </v-form>
+      </v-card-text>
+    </v-card>
+
+    <div class="ma-3 mt-5 pt-3">
+      <v-btn
+        @click="submit()"
+        size="large"
+        color="primary"
+        variant="flat"
+        class="w-100"
+        :loading="loading"
+        prepend-icon="translate_variant"
+      >
+        {{ t("translate") }}
+      </v-btn>
+
+      <v-btn
+        v-if="zipFile"
+        @click="downloadZipFile()"
+        size="large"
+        color="primary"
+        variant="outlined"
+        class="w-100 mt-5"
+        prepend-icon="download"
+      >
+        {{ t("download") }}
+      </v-btn>
+    </div>
+  </v-container>
+</template>
+
+<style lang="scss" scoped></style>