소스 검색

Merge branch 'front-dev' of http://git.choozmo.com:3000/ai-anchor/video-maker

tomoya 1 년 전
부모
커밋
b79228b75d
57개의 변경된 파일460개의 추가작업 그리고 191개의 파일을 삭제
  1. 1 1
      backend/app/app/api/api_v1/endpoints/ytviewspayment.py
  2. 5 3
      frontend/src/App.vue
  3. 13 4
      frontend/src/api.ts
  4. BIN
      frontend/src/assets/img/anchor/Jocelyn.webp
  5. BIN
      frontend/src/assets/img/anchor/Peggy.webp
  6. BIN
      frontend/src/assets/img/anchor/Summer.webp
  7. 0 0
      frontend/src/assets/img/anchor/angela.png
  8. BIN
      frontend/src/assets/img/anchor/jocelyn.png
  9. BIN
      frontend/src/assets/img/anchor/peggy.png
  10. BIN
      frontend/src/assets/img/anchor/summer.png
  11. BIN
      frontend/src/assets/img/anchor/半身主播-1.webp
  12. BIN
      frontend/src/assets/img/anchor/半身主播-10.webp
  13. BIN
      frontend/src/assets/img/anchor/半身主播-11.webp
  14. BIN
      frontend/src/assets/img/anchor/半身主播-12.webp
  15. BIN
      frontend/src/assets/img/anchor/半身主播-13.webp
  16. BIN
      frontend/src/assets/img/anchor/半身主播-14.webp
  17. BIN
      frontend/src/assets/img/anchor/半身主播-15.webp
  18. BIN
      frontend/src/assets/img/anchor/半身主播-16.webp
  19. BIN
      frontend/src/assets/img/anchor/半身主播-2.webp
  20. BIN
      frontend/src/assets/img/anchor/半身主播-3.webp
  21. BIN
      frontend/src/assets/img/anchor/半身主播-4.webp
  22. BIN
      frontend/src/assets/img/anchor/半身主播-5.webp
  23. BIN
      frontend/src/assets/img/anchor/半身主播-6.webp
  24. BIN
      frontend/src/assets/img/anchor/半身主播-7.webp
  25. BIN
      frontend/src/assets/img/anchor/半身主播-8.webp
  26. BIN
      frontend/src/assets/img/anchor/半身主播-9.webp
  27. BIN
      frontend/src/assets/img/template/舊/鏡面-01.png
  28. 0 0
      frontend/src/assets/img/template/舊/鏡面-01.webp
  29. 0 0
      frontend/src/assets/img/template/舊/鏡面-02.webp
  30. 0 0
      frontend/src/assets/img/template/舊/鏡面-03.webp
  31. 0 0
      frontend/src/assets/img/template/舊/鏡面-04.webp
  32. 0 0
      frontend/src/assets/img/template/舊/鏡面-05.webp
  33. 0 0
      frontend/src/assets/img/template/舊/鏡面-06.webp
  34. BIN
      frontend/src/assets/img/template/鏡面-01.png
  35. BIN
      frontend/src/assets/img/template/鏡面-02.png
  36. BIN
      frontend/src/assets/img/template/鏡面-03.png
  37. BIN
      frontend/src/assets/img/template/鏡面-04.png
  38. 3 2
      frontend/src/interfaces/index.ts
  39. 7 1
      frontend/src/language/en.json
  40. 7 1
      frontend/src/language/zh.json
  41. 10 0
      frontend/src/router/index.ts
  42. 14 3
      frontend/src/stores/main.ts
  43. 17 6
      frontend/src/utils.ts
  44. 146 0
      frontend/src/views/Orders.vue
  45. 1 1
      frontend/src/views/PasswordRecovery.vue
  46. 1 1
      frontend/src/views/Qrcode.vue
  47. 23 8
      frontend/src/views/YTViews.vue
  48. 1 1
      frontend/src/views/main/Article.vue
  49. 81 0
      frontend/src/views/main/Contact.vue
  50. 3 6
      frontend/src/views/main/Dashboard.vue
  51. 12 9
      frontend/src/views/main/Main.vue
  52. 2 2
      frontend/src/views/main/Start.vue
  53. 41 121
      frontend/src/views/main/Upload.vue
  54. 1 1
      frontend/src/views/main/admin/CreateUser.vue
  55. 1 1
      frontend/src/views/main/admin/EditUser.vue
  56. 1 1
      frontend/src/views/main/admin/TestCelery.vue
  57. 69 18
      frontend/src/views/main/profile/UserProfileEdit.vue

+ 1 - 1
backend/app/app/api/api_v1/endpoints/ytviewspayment.py

@@ -325,4 +325,4 @@ CheckMacValue: {CheckMacValue}\
     remark = json.loads(ytviews.remark)
     remark['TradeNo'] = TradeNo
     crud.ytviews.update(db, db_obj=ytviews, obj_in={"payment_state":"succeeded", "remark":json.dumps(remark, ensure_ascii=False)})
-  return Response(content=1, status_code=status.HTTP_200_OK)
+  return Response(content='1', status_code=status.HTTP_200_OK)

+ 5 - 3
frontend/src/App.vue

@@ -15,7 +15,7 @@ const loggedIn = mainStoreRef.readIsLoggedIn;
 //lifecycle
 onMounted(() => {
   let path = location.pathname;
-  if (path !== "/qrcode") {
+  if (path !== "/qrcode" && path !== "/yt-views" && path !== "/orders") {
     mainStore.checkLoggedIn();
   }
 });
@@ -43,14 +43,16 @@ a {
   }
 }
 
+.v-input__details {
+  padding: 3px 16px;
+}
+
 .login-form {
   margin: auto;
   max-width: 500px;
   position: relative;
 
   .v-input__details {
-    padding: 3px;
-
     .v-messages {
       color: #b00020 !important;
       text-align: end;

+ 13 - 4
frontend/src/api.ts

@@ -81,8 +81,9 @@ export const api = {
   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())
-    formData.append("lang_id", video_data.lang_id.toString())
+    formData.append("anchor", video_data.anchor)
+    formData.append("style", video_data.style)
+    formData.append("lang", video_data.lang)
     formData.append("upload_file", file)
     return axios.post<VideoUploaded>(`${apiUrl}/api/v1/videos/`, formData, authHeaders(token));
   },
@@ -140,10 +141,18 @@ export const api = {
     formData.append("amount", amount.toString());
     return axios.post<string>(`${apiUrl}/api/v1/payment/ecpayTestPay`, formData, authHeaders(token))
   },
-  async YTViewsPayment(user_data: YTViewsUserData) {
-    return axios.post<string>(`${apiUrl}/api/v1/payment/ytviews-ecpay-payment`, user_data);
+  async YTViewsPayment(user_data: YTViewsUserData, lang: string) {
+    const config = {
+      params: {
+        lang: lang
+      }
+    };
+    return axios.post<string>(`${apiUrl}/api/v1/payment/ytviews-ecpay-payment`, user_data, config);
   },
   async YTViewsTestPayment(user_data: YTViewsUserData) {
     return axios.post<string>(`${apiUrl}/api/v1/payment/ytviews-ecpay-test-payment`, user_data);
   },
+  async getYTViewsList() {
+    return axios.get(`${apiUrl}/api/v1/payment/ytviews-list-all`);
+  },
 };

BIN
frontend/src/assets/img/anchor/Jocelyn.webp


BIN
frontend/src/assets/img/anchor/Peggy.webp


BIN
frontend/src/assets/img/anchor/Summer.webp


+ 0 - 0
frontend/src/assets/img/anchor/Angela.webp → frontend/src/assets/img/anchor/angela.png


BIN
frontend/src/assets/img/anchor/jocelyn.png


BIN
frontend/src/assets/img/anchor/peggy.png


BIN
frontend/src/assets/img/anchor/summer.png


BIN
frontend/src/assets/img/anchor/半身主播-1.webp


BIN
frontend/src/assets/img/anchor/半身主播-10.webp


BIN
frontend/src/assets/img/anchor/半身主播-11.webp


BIN
frontend/src/assets/img/anchor/半身主播-12.webp


BIN
frontend/src/assets/img/anchor/半身主播-13.webp


BIN
frontend/src/assets/img/anchor/半身主播-14.webp


BIN
frontend/src/assets/img/anchor/半身主播-15.webp


BIN
frontend/src/assets/img/anchor/半身主播-16.webp


BIN
frontend/src/assets/img/anchor/半身主播-2.webp


BIN
frontend/src/assets/img/anchor/半身主播-3.webp


BIN
frontend/src/assets/img/anchor/半身主播-4.webp


BIN
frontend/src/assets/img/anchor/半身主播-5.webp


BIN
frontend/src/assets/img/anchor/半身主播-6.webp


BIN
frontend/src/assets/img/anchor/半身主播-7.webp


BIN
frontend/src/assets/img/anchor/半身主播-8.webp


BIN
frontend/src/assets/img/anchor/半身主播-9.webp


BIN
frontend/src/assets/img/template/舊/鏡面-01.png


+ 0 - 0
frontend/src/assets/img/template/鏡面-01.webp → frontend/src/assets/img/template/舊/鏡面-01.webp


+ 0 - 0
frontend/src/assets/img/template/鏡面-02.webp → frontend/src/assets/img/template/舊/鏡面-02.webp


+ 0 - 0
frontend/src/assets/img/template/鏡面-03.webp → frontend/src/assets/img/template/舊/鏡面-03.webp


+ 0 - 0
frontend/src/assets/img/template/鏡面-04.webp → frontend/src/assets/img/template/舊/鏡面-04.webp


+ 0 - 0
frontend/src/assets/img/template/鏡面-05.webp → frontend/src/assets/img/template/舊/鏡面-05.webp


+ 0 - 0
frontend/src/assets/img/template/鏡面-06.webp → frontend/src/assets/img/template/舊/鏡面-06.webp


BIN
frontend/src/assets/img/template/鏡面-01.png


BIN
frontend/src/assets/img/template/鏡面-02.png


BIN
frontend/src/assets/img/template/鏡面-03.png


BIN
frontend/src/assets/img/template/鏡面-04.png


+ 3 - 2
frontend/src/interfaces/index.ts

@@ -51,8 +51,9 @@ export interface Video {
 
 export interface VideoCreate {
   title: string;
-  anchor_id: number;
-  lang_id: number;
+  anchor: string;
+  style: string;
+  lang: string;
 }
 
 export interface VideoUploaded {

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

@@ -64,5 +64,11 @@
     "sendingEmail": "Sending password recovery email",
     "passwordMailSent": "Password recovery email sent",
     "acceptZipMessage": "Video processing takes about 5-10 minutes, please be patient",
-    "ytViews": "YouTube Views"
+    "ytViews": "YouTube Views",
+    "contactUs": "Contact Us",
+    "orderDetails": "Order Details",
+    "required": "This field is required.",
+    "emailRules": "Must be a valid e-mail.",
+    "phoneNumber": "Phone Number (optional)",
+    "feedback": "Feedback"
 }

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

@@ -64,5 +64,11 @@
     "sendingEmail": "傳送電子郵件中",
     "passwordMailSent": "重置密碼電子郵件已傳送",
     "acceptZipMessage": "影片處理需要約 5-10 分鐘,敬請耐心等候",
-    "ytViews": "網紅加速器"
+    "ytViews": "網紅加速器",
+    "contactUs": "聯絡我們",
+    "orderDetails": "訂單明細",
+    "required": "此為必填欄位",
+    "emailRules": "請輸入有效的電子郵件格式",
+    "phoneNumber": "手機號碼(非必填)",
+    "feedback": "意見回饋"
 }

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

@@ -43,6 +43,11 @@ const router = createRouter({
           name: 'yt-views',
           component: () => import('@/views/YTViews.vue'),
         },
+        {
+          path: 'orders',
+          name: 'orders',
+          component: () => import('@/views/Orders.vue'),
+        },
         {
           path: 'test-yt-views',
           name: 'test-yt-views',
@@ -83,6 +88,11 @@ const router = createRouter({
             //   name: 'yt-views',
             //   component: () => import('@/views/main/YTViews.vue'),
             // },
+            {
+              path: 'contact',
+              name: 'contact',
+              component: () => import('@/views/main/Contact.vue'),
+            },
             {
               path: 'profile',
               name: 'profile',

+ 14 - 3
frontend/src/stores/main.ts

@@ -175,7 +175,7 @@ export const useMainStore = defineStore("MainStoreId", {
         }
       }
     },
-    async qrCheckLoggedIn() {
+    async cancelCheckLoggedIn() {
       if (!this.isLoggedIn) {
         let token = this.token;
         if (!token) {
@@ -481,12 +481,12 @@ export const useMainStore = defineStore("MainStoreId", {
         await mainStore.checkApiError(error);
       }
     },
-    async YTViewsPayment(user_data: YTViewsUserData) {
+    async YTViewsPayment(user_data: YTViewsUserData, lang: string) {
       const mainStore = useMainStore();
       try {
         const response = (
           await Promise.all([
-            api.YTViewsPayment(user_data),
+            api.YTViewsPayment(user_data, lang),
             await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 0)),
           ])
         );
@@ -497,5 +497,16 @@ export const useMainStore = defineStore("MainStoreId", {
         await mainStore.checkApiError(error);
       }
     },
+    async getYTViewsList() {
+      const mainStore = useMainStore();
+      try {
+        const response = await api.getYTViewsList();
+        if (response) {
+          return response.data;
+        }
+      } catch (error) {
+        await mainStore.checkApiError(error);
+      }
+    },
   }
 });

+ 17 - 6
frontend/src/utils.ts

@@ -6,14 +6,25 @@ export const removeLocalToken = () => localStorage.removeItem("token");
 
 
 import type { Ref } from "vue";
+import { useI18n } from "vue-i18n";
 
-export const emailRules =  [
-  (v:any) => /^[a-z.0-9]+@[a-z.-]+\.[a-z]+$/i.test(v) || 'Must be a valid e-mail.',
-];
+// export const emailRules =  [
+//   (v:any) => /^[a-z.0-9]+@[a-z.-]+\.[a-z]+$/i.test(v) || 'Must be a valid e-mail.',
+// ];
 
-export const required = [
-  (v:any) => !!v || 'This field is required.',
-];
+// export const required = [
+//   (v:any) => !!v || ('This field is required.'),
+// ];
+
+export function emailRules() {
+  const { t } = useI18n();
+  return [(v:any) => /^[a-z.0-9]+@[a-z.-]+\.[a-z]+$/i.test(v) || t('emailRules')];
+}
+
+export function required() {
+  const { t } = useI18n();
+  return [(v: any) => !!v || t('required')];
+}
 
 export const nameRules = [
   (v:any) => !!v || 'Name is required.',

+ 146 - 0
frontend/src/views/Orders.vue

@@ -0,0 +1,146 @@
+<script setup lang="ts">
+import { useMainStore } from "@/stores/main";
+import { reactive } from "vue";
+import { useI18n } from "vue-i18n";
+
+const { t } = useI18n();
+const mainStore = useMainStore();
+let orders: any[] = reactive([]);
+
+mainStore
+  .getYTViewsList()
+  .then((result) => {
+    console.log(result);
+    if (result) {
+      orders.push(result);
+    }
+  })
+  .catch((error) => {
+    console.error(error);
+  });
+
+console.log("orders", orders);
+
+const headers = [
+  {
+    title: "付款狀態",
+    sortable: true,
+    key: "payment_state",
+    align: "left",
+  },
+  {
+    title: "ID",
+    sortable: true,
+    key: "id",
+    align: "left",
+  },
+  {
+    title: "商品金額",
+    sortable: true,
+    key: "amount",
+    align: "left",
+  },
+  {
+    title: "姓名",
+    sortable: true,
+    key: "name",
+    align: "left",
+  },
+  {
+    title: "電子郵件",
+    sortable: true,
+    key: "email",
+    align: "left",
+  },
+  {
+    title: "公司 / 所屬產業",
+    sortable: true,
+    key: "company",
+    align: "left",
+  },
+  {
+    title: "YouTube 影片網址",
+    sortable: true,
+    key: "url",
+    align: "left",
+  },
+  {
+    title: "影片放送地區",
+    sortable: true,
+    key: "area",
+    align: "left",
+  },
+  {
+    title: "受眾語言",
+    sortable: true,
+    key: "language",
+    align: "left",
+  },
+  {
+    title: "客層",
+    sortable: true,
+    key: "ages",
+    align: "left",
+  },
+  {
+    title: "目標對象區隔",
+    sortable: true,
+    key: "target",
+    align: "left",
+  },
+  {
+    title: "影片主題",
+    sortable: true,
+    key: "theme",
+    align: "left",
+  },
+  {
+    title: "統編號碼",
+    sortable: true,
+    key: "taxID",
+    align: "left",
+  },
+];
+</script>
+
+<template>
+  <div>
+    <v-toolbar light>
+      <v-toolbar-title>
+        <h3>訂單明細</h3>
+      </v-toolbar-title>
+    </v-toolbar>
+    <v-data-table
+      :headers="headers"
+      :items="orders[0]"
+      :sort-by="[{ key: 'id', order: 'desc' }]"
+    >
+      <template v-slot:item.payment_state="{ item }">
+        <span v-if="item.raw.payment_state === 'succeeded'">
+          <v-icon icon="check_circle" color="success" />
+          完成
+        </span>
+        <span v-else-if="item.raw.payment_state === 'waiting'">
+          <v-icon icon="pending" color="warning" />
+          處理中
+        </span>
+        <span v-else>
+          <v-icon icon="warning" color="error" />
+          失敗
+        </span>
+      </template>
+    </v-data-table>
+  </div>
+</template>
+
+<style lang="scss">
+.v-data-table-footer {
+  margin-top: 10px;
+}
+.v-table {
+  td,
+  th {
+    white-space: nowrap;
+  }
+}
+</style>

+ 1 - 1
frontend/src/views/PasswordRecovery.vue

@@ -56,7 +56,7 @@ function submit() {
                 type="text"
                 prepend-icon="email"
                 v-model="email"
-                :rules="required"
+                :rules="required()"
                 required
               ></v-text-field>
             </v-form>

+ 1 - 1
frontend/src/views/Qrcode.vue

@@ -91,7 +91,7 @@ onMounted(() => {
   if (route.query["add_time_code"]) {
     ser_no.value = route.query["add_time_code"];
   }
-  mainStore.qrCheckLoggedIn();
+  mainStore.cancelCheckLoggedIn();
   if (mainStore.token) {
     checkCode("");
   } else {

+ 23 - 8
frontend/src/views/YTViews.vue

@@ -1,11 +1,15 @@
 <script setup lang="ts">
 import { ref, reactive, computed } from "vue";
-import type { YTViewsUserData } from "@/interfaces";
+import { required, emailRules } from "@/utils";
 import { useMainStore } from "@/stores/main";
+import type { YTViewsUserData } from "@/interfaces";
 import Navbar from "@/components/Navbar.vue";
 
 const mainStore = useMainStore();
-const fieldRules = [(value: string) => !!value || "此欄位為必填項目"];
+const urlRules = [
+  (v: any) =>
+    /^(http|https):\/\//.test(v) || "請輸入以 http 或 https 開頭的有效網址",
+];
 
 const items = reactive([
   { title: "100% 真人觀看" },
@@ -136,6 +140,16 @@ async function ECPaySubmit() {
     userData.theme = theme.value;
   }
 
+  let lang = ref("");
+  let getLang = localStorage.getItem("lang");
+
+  // 綠界顯示語言
+  if (getLang === "zh") {
+    lang.value = "ZH";
+  } else {
+    lang.value = "ENG";
+  }
+
   let data: YTViewsUserData = {
     item: `YT0.4-(${assignView.value})`,
     amount: assignPrice.value,
@@ -150,7 +164,8 @@ async function ECPaySubmit() {
     theme: userData.theme,
     taxID: userData.taxID,
   };
-  const originalHTML = await mainStore.YTViewsTestPayment(data);
+
+  const originalHTML = await mainStore.YTViewsPayment(data, lang.value);
   let formHTML = originalHTML?.replace(
     '<script type="text/javascript">document.getElementById("data_set").submit();</scr',
     ""
@@ -268,13 +283,13 @@ async function ECPaySubmit() {
           <v-form @submit.prevent class="ECPay-form">
             <v-text-field
               v-model="userData.email"
-              :rules="fieldRules"
+              :rules="emailRules()"
               label="電子郵件"
               required
             ></v-text-field>
             <v-text-field
               v-model="userData.name"
-              :rules="fieldRules"
+              :rules="required()"
               label="姓名"
               required
             ></v-text-field>
@@ -284,19 +299,19 @@ async function ECPaySubmit() {
             ></v-text-field>
             <v-text-field
               v-model="userData.url"
-              :rules="fieldRules"
+              :rules="urlRules"
               label="YouTube 影片網址"
               required
             ></v-text-field>
             <v-text-field
               v-model="userData.area"
-              :rules="fieldRules"
+              :rules="required()"
               label="影片放送地區(國家 / 縣市)"
               required
             ></v-text-field>
             <v-text-field
               v-model="userData.language"
-              :rules="fieldRules"
+              :rules="required()"
               label="受眾語言"
               required
             ></v-text-field>

+ 1 - 1
frontend/src/views/main/Article.vue

@@ -33,7 +33,7 @@ async function Submit() {
           <v-text-field
             :label="$t('articleTitle')"
             v-model="title"
-            :rules="required"
+            :rules="required()"
             prepend-icon="title"
           >
           </v-text-field>

+ 81 - 0
frontend/src/views/main/Contact.vue

@@ -0,0 +1,81 @@
+<script setup lang="ts">
+import { ref } from "vue";
+import { useMainStore } from "@/stores/main";
+import { storeToRefs } from "pinia";
+import { required, emailRules } from "@/utils";
+import { useI18n } from "vue-i18n";
+
+const { t } = useI18n();
+const valid = ref(true);
+const fullName = ref("");
+const email = ref("");
+const phone = ref("");
+const feedback = ref("");
+const form = ref(null);
+
+const phoneRules = [(v: any) => /^(09\d{8})?$/i.test(v) || "請輸入有效的號碼"];
+
+const mainStore = useMainStore();
+const mainStoreRef = storeToRefs(mainStore);
+const userProfile = mainStoreRef.readUserProfile;
+
+if (userProfile) {
+  if (typeof userProfile.value?.full_name === "string") {
+    fullName.value = userProfile.value.full_name;
+  } else {
+    fullName.value = "";
+  }
+  email.value = userProfile.value!.email;
+}
+
+async function submit() {
+  await (form as any).value.validate();
+  //   if (valid.value) {
+  //     await mainStore.updateUserProfile(updateProfile);
+  //   }
+}
+</script>
+
+<template>
+  <v-container fluid>
+    <v-card class="ma-3 pa-3">
+      <v-card-title>
+        <h3 class="card-title">{{ t("contactUs") }}</h3>
+      </v-card-title>
+      <v-card-text class="pt-3">
+        <v-form ref="form" v-model="valid">
+          <v-text-field
+            :label="$t('userName')"
+            v-model="fullName"
+            :rules="required()"
+            required
+          ></v-text-field>
+          <v-text-field
+            :label="$t('emailAddress')"
+            type="email"
+            v-model="email"
+            :rules="emailRules()"
+            required
+          ></v-text-field>
+          <v-text-field
+            :label="$t('phoneNumber')"
+            type="tel"
+            v-model="phone"
+            :rules="phoneRules"
+          ></v-text-field>
+          <v-textarea
+            :label="$t('feedback')"
+            v-model="feedback"
+            :rules="required()"
+            placeholder="請告訴我們您的使用體驗,您可以在此回報問題、提出功能建議,我們廣納接受各式建議與想法,請從填寫簡短描述開始"
+            required
+          ></v-textarea>
+        </v-form>
+      </v-card-text>
+      <v-card-actions>
+        <v-spacer></v-spacer>
+        <v-btn @click="submit">{{ t("send") }}</v-btn>
+      </v-card-actions>
+    </v-card>
+  </v-container>
+</template>

+ 3 - 6
frontend/src/views/main/Dashboard.vue

@@ -51,13 +51,13 @@ const greetedUser = computed(() => {
             <v-btn to="/main/profile/edit" variant="outlined" color="primary">{{
               t("edit")
             }}</v-btn>
-            <v-btn
+            <!-- <v-btn
               to="/main/profile/password"
               variant="flat"
               color="primary"
               class="ms-3"
               >{{ t("changePassword") }}</v-btn
-            >
+            > -->
           </v-card-actions>
           <!-- <v-card-actions>
         <v-btn to="/main/profile/view">View Profile</v-btn>
@@ -93,10 +93,7 @@ const greetedUser = computed(() => {
           <v-card-title primary-title>
             <h3>可使用秒數</h3>
           </v-card-title>
-          <v-card-text
-            v-if="userProfile"
-            class="mt-3"
-          >
+          <v-card-text v-if="userProfile" class="mt-3">
             <strong>{{ userProfile.available_time }}</strong
             ><small>秒</small>
           </v-card-text>

+ 12 - 9
frontend/src/views/main/Main.vue

@@ -111,27 +111,30 @@ const routeGuardAdmin = async (
             <v-list-item to="/main/progress" prepend-icon="list">
               <v-list-item-title>{{ t("progress") }}</v-list-item-title>
             </v-list-item>
-            <!-- <v-list-item to="/main/make-article" prepend-icon="article">
-              <v-list-item-title>{{ t("article") }}</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>
             </v-list-item> -->
+            <!-- <v-list-item to="/main/make-article" prepend-icon="article">
+              <v-list-item-title>{{ t("article") }}</v-list-item-title>
+            </v-list-item> -->
             <!-- <v-list-item to="/main/profile/view" prepend-icon="person">
               <v-list-item-title>{{ t("userProfile") }}</v-list-item-title>
             </v-list-item> -->
             <!-- <v-list-item to="/main/yt-views" prepend-icon="ondemand_video">
-              <v-list-item-title>網紅加速器</v-list-item-title>
+              <v-list-item-title>{{ t("ytViews") }}</v-list-item-title>
             </v-list-item> -->
+            <v-list-item to="/main/contact" prepend-icon="email">
+              <v-list-item-title>{{ t("contactUs") }}</v-list-item-title>
+            </v-list-item>
             <v-list-item to="/main/profile/edit" prepend-icon="edit">
               <v-list-item-title>{{ t("editProfile") }}</v-list-item-title>
             </v-list-item>
-            <v-list-item to="/main/profile/password" prepend-icon="key">
+            <!-- <v-list-item to="/main/profile/password" prepend-icon="key">
               <v-list-item-title>{{ t("changePassword") }}</v-list-item-title>
-            </v-list-item>
+            </v-list-item> -->
           </v-list>
         </v-sheet>
-        <v-divider></v-divider>
+        <!-- <v-divider></v-divider> -->
         <v-sheet class="">
           <v-list subheader v-show="hasAdminAccess">
             <v-list-subheader
@@ -238,7 +241,7 @@ const routeGuardAdmin = async (
     <v-footer class="pa-3" app>
       <v-spacer></v-spacer>
       <div class="contact-icon">
-        <a href="https://discord.gg/kHAEcu8T">
+        <a href="https://discord.gg/C6Yk83ZA" target="_blank">
           <img src="@/assets/img/icon/discord.png" alt="discord" />
         </a>
         <a href="mailto:service@choozmo.com">
@@ -281,7 +284,7 @@ const routeGuardAdmin = async (
 .contact-icon {
   margin-top: 10px;
   img {
-    width: 35px;
+    width: 30px;
     margin: 0 5px;
   }
 }

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

@@ -27,9 +27,9 @@ const startRouteGuard = async (
   const mainStore = useMainStore();
   const mainStoreRef = storeToRefs(mainStore);
 
-  if (to.path === "/qrcode") {
+  if (to.path === "/qrcode" || to.path === "/yt-views" || to.path === "/orders") {
     next();
-    mainStore.qrCheckLoggedIn();
+    mainStore.cancelCheckLoggedIn();
   } else {
     mainStore.checkLoggedIn();
   }

+ 41 - 121
frontend/src/views/main/Upload.vue

@@ -18,6 +18,8 @@ const zipFiles = ref();
 const Form = ref();
 let anchor = ref(0);
 let templateId = ref(0);
+let selectAnchor = ref("angela");
+let selectTemplate = ref("style1");
 
 // props
 let dialog = reactive({
@@ -28,132 +30,40 @@ let dialog = reactive({
 
 const anchorList = reactive([
   {
-    anchor_id: 0,
-    language_id: 1,
+    anchor_id: "angela",
     name: "Angela",
   },
   {
-    anchor_id: 1,
-    language_id: 1,
-    name: "半身主播-1",
-  },
-  {
-    anchor_id: 2,
-    language_id: 1,
-    name: "半身主播-2",
-  },
-  {
-    anchor_id: 3,
-    language_id: 1,
-    name: "半身主播-3",
-  },
-  {
-    anchor_id: 4,
-    language_id: 1,
-    name: "半身主播-4",
-  },
-  {
-    anchor_id: 5,
-    language_id: 1,
-    name: "半身主播-5",
-  },
-  {
-    anchor_id: 6,
-    language_id: 1,
-    name: "半身主播-6",
-  },
-  {
-    anchor_id: 7,
-    language_id: 1,
-    name: "半身主播-7",
-  },
-  {
-    anchor_id: 8,
-    language_id: 1,
-    name: "半身主播-8",
-  },
-  {
-    anchor_id: 9,
-    language_id: 1,
-    name: "半身主播-9",
-  },
-  {
-    anchor_id: 10,
-    language_id: 1,
-    name: "半身主播-10",
-  },
-  {
-    anchor_id: 11,
-    language_id: 1,
-    name: "半身主播-11",
-  },
-  {
-    anchor_id: 12,
-    language_id: 1,
-    name: "半身主播-12",
-  },
-  {
-    anchor_id: 13,
-    language_id: 1,
-    name: "半身主播-13",
-  },
-  {
-    anchor_id: 14,
-    language_id: 1,
-    name: "半身主播-14",
-  },
-  {
-    anchor_id: 15,
-    language_id: 1,
-    name: "半身主播-15",
+    anchor_id: "jocelyn",
+    name: "Jocelyn",
   },
   {
-    anchor_id: 16,
-    language_id: 1,
-    name: "半身主播-16",
+    anchor_id: "summer",
+    name: "Summer",
   },
   {
-    anchor_id: 17,
-    language_id: 1,
+    anchor_id: "peggy",
     name: "Peggy",
   },
-  {
-    anchor_id: 18,
-    language_id: 1,
-    name: "Jocelyn",
-  },
-  {
-    anchor_id: 19,
-    language_id: 1,
-    name: "Summer",
-  },
 ]);
 
 const templateList = reactive([
   {
-    template_id: 0,
+    template_id: "style1",
     img: "鏡面-01",
   },
   {
-    template_id: 1,
+    template_id: "style2",
     img: "鏡面-02",
   },
   {
-    template_id: 2,
+    template_id: "style3",
     img: "鏡面-03",
   },
   {
-    template_id: 3,
+    template_id: "style4",
     img: "鏡面-04",
   },
-  {
-    template_id: 4,
-    img: "鏡面-05",
-  },
-  {
-    template_id: 5,
-    img: "鏡面-06",
-  },
 ]);
 
 let anchorLang = ref("中文");
@@ -164,11 +74,11 @@ let items = reactive([
 
 // 取得圖片路徑
 const getImageUrl = (imgFolder: string, name: string) => {
-  return new URL(`../../assets/img/${imgFolder}/${name}.webp`, import.meta.url)
+  return new URL(`../../assets/img/${imgFolder}/${name}.png`, import.meta.url)
     .href;
 };
 
-watch(dialog, (newVal, oldVal) => {
+watch(dialog, (newVal) => {
   if (!newVal.show && newVal.state === "error") {
     return;
   } else if (!newVal.show && newVal.state === "success") {
@@ -176,6 +86,14 @@ watch(dialog, (newVal, oldVal) => {
   }
 });
 
+watch(anchor, (newVal) => {
+  selectAnchor.value = anchorList[newVal].anchor_id;
+});
+
+watch(templateId, (newVal) => {
+  selectTemplate.value = templateList[newVal].template_id;
+});
+
 async function Submit() {
   WS.send("subscribe");
   await (Form as any).value.validate();
@@ -184,22 +102,28 @@ async function Submit() {
 
     const video_data: VideoCreate = {
       title: title.value,
-      anchor_id: anchor.value,
-      lang_id: 0,
+      anchor: selectAnchor.value,
+      style: selectTemplate.value,
+      lang: "zh",
     };
 
-    const ret:VideoUploaded = await mainStore.uploadPlot(video_data, zipFiles.value[0]);
+    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 {
+    } else {
       dialog.msg = ret.error_message!;
       dialog.state = "error";
       dialog.show = true;
     }
+
     valid.value = true;
+
     // (Form as any).value.reset();
   }
 }
@@ -216,7 +140,7 @@ async function Submit() {
           <v-text-field
             :label="$t('videoTitle')"
             v-model="title"
-            :rules="required"
+            :rules="required()"
             prepend-icon="title"
           >
           </v-text-field>
@@ -252,20 +176,17 @@ 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">
+                            <!-- <div v-if="n.anchor_id !== 0" class="img-disabled">
                               <img
                                 :src="getImageUrl('anchor', n.name)"
                                 alt=""
                               />
                               <p>Coming Soon</p>
-                            </div>
-
+                            </div> -->
                             <img
-                              v-else
-                              :src="getImageUrl('anchor', n.name)"
+                              :src="getImageUrl('anchor', n.anchor_id)"
                               alt=""
                             />
                           </v-scroll-y-transition>
@@ -292,7 +213,6 @@ async function Submit() {
                     v-for="n in templateList"
                     :key="n.template_id"
                     v-slot="{ isSelected, toggle, selectedClass }"
-                    :disabled="n.template_id !== 0"
                   >
                     <v-card
                       color="grey-lighten-1"
@@ -305,11 +225,11 @@ async function Submit() {
                       >
                         <v-icon icon="done" color="white" />
                       </span>
-                      <div :class="{ 'img-disabled': n.template_id !== 0 }">
+                      <img :src="getImageUrl('template', n.img)" alt="" />
+                      <!-- <div :class="{ 'img-disabled': n.template_id !== 0 }">
                         <img :src="getImageUrl('template', n.img)" alt="" />
                         <p v-if="n.template_id !== 0">Coming Soon</p>
-                      </div>
-                      <!-- <img :src="getImageUrl('template', n.img)" alt="" /> -->
+                      </div> -->
                     </v-card>
                   </v-slide-group-item>
                 </v-slide-group>
@@ -550,4 +470,4 @@ async function Submit() {
 .v-card--disabled > :not(.v-card__loader) {
   opacity: 1 !important;
 }
-</style>
+</style>

+ 1 - 1
frontend/src/views/main/admin/CreateUser.vue

@@ -7,7 +7,7 @@
       <v-card-text>
           <v-form v-model="valid" ref="form">
             <v-text-field label="Full Name" v-model="fullName" :rules="nameRules" required></v-text-field>
-            <v-text-field label="E-mail" type="email" v-model="email" :rules="emailRules" required></v-text-field>
+            <v-text-field label="E-mail" type="email" v-model="email" :rules="emailRules()" required></v-text-field>
             <div class="subheading secondary--text text--lighten-2">
               User is superuser 
               <span v-if="isSuperuser">(currently is a superuser)</span>

+ 1 - 1
frontend/src/views/main/admin/EditUser.vue

@@ -29,7 +29,7 @@
               label="E-mail"
               type="email"
               v-model="email"
-              :rules="emailRules"
+              :rules="emailRules()"
             ></v-text-field>
             <div class="subheading secondary--text text--lighten-2">User is superuser <span v-if="isSuperuser">(currently is a superuser)</span><span v-else>(currently is not a superuser)</span></div>
             <v-checkbox

+ 1 - 1
frontend/src/views/main/admin/TestCelery.vue

@@ -9,7 +9,7 @@
             <v-text-field 
               @keyup.enter="msgSubmit" 
               label="Message" v-model="msg" 
-              :rules="required"
+              :rules="required()"
               prepend-icon="message">
             </v-text-field>
           </v-form>

+ 69 - 18
frontend/src/views/main/profile/UserProfileEdit.vue

@@ -4,18 +4,28 @@ import { useMainStore } from "@/stores/main";
 import { storeToRefs } from "pinia";
 import router from "@/router";
 import type { IUserProfileUpdate } from "@/interfaces";
-import { emailRules, nameRules } from "@/utils";
+import {
+  emailRules,
+  nameRules,
+  password1Rules,
+  usePassword2Rules,
+} from "@/utils";
 import { useI18n } from "vue-i18n";
 
 const { t } = useI18n();
+const mainStore = useMainStore();
+const mainStoreRef = storeToRefs(mainStore);
+const userProfile = mainStoreRef.readUserProfile;
+
 const valid = ref(true);
+const validPassword = ref(true);
 const fullName = ref("");
 const email = ref("");
 const form = ref(null);
+const password1 = ref("");
+const password2 = ref("");
+const password2Rules = usePassword2Rules(password1);
 
-const mainStore = useMainStore();
-const mainStoreRef = storeToRefs(mainStore);
-const userProfile = mainStoreRef.readUserProfile;
 if (userProfile) {
   if (typeof userProfile.value?.full_name === "string") {
     fullName.value = userProfile.value.full_name;
@@ -36,10 +46,16 @@ function reset() {
   }
 }
 
+function resetPassword() {
+  password1.value = "";
+  password2.value = "";
+}
+
 function cancel() {
   router.back();
 }
 
+// 編輯資料
 async function submit() {
   await (form as any).value.validate();
   if (valid.value) {
@@ -51,13 +67,24 @@ async function submit() {
       updateProfile.email = email.value;
     }
     await mainStore.updateUserProfile(updateProfile);
-    router.push("/main/profile");
+    router.push("/main/dashboard");
+  }
+}
+
+// 變更密碼
+async function submitPassword() {
+  await (form as any).value.validate();
+  if (validPassword.value) {
+    const updatedProfile: IUserProfileUpdate = {};
+    updatedProfile.password = password1.value;
+    await mainStore.updateUserProfile(updatedProfile);
+    router.push("/main/dashboard");
   }
 }
 
-onMounted(() => {
-  (form as any).value.validate();
-});
+// onMounted(() => {
+//   (form as any).value.validate();
+// });
 </script>
 
 <template>
@@ -67,7 +94,7 @@ onMounted(() => {
         <h3 class="card-title">{{ t("editUserProfile") }}</h3>
       </v-card-title>
       <v-card-text class="pt-3">
-        <v-form v-model="valid" ref="form">
+        <v-form ref="form" v-model="valid">
           <v-text-field
             :label="$t('userName')"
             v-model="fullName"
@@ -78,7 +105,7 @@ onMounted(() => {
             :label="$t('emailAddress')"
             type="email"
             v-model="email"
-            :rules="emailRules"
+            :rules="emailRules()"
             required
           ></v-text-field>
         </v-form>
@@ -90,13 +117,37 @@ onMounted(() => {
         <v-btn @click="submit" :disabled="!valid">{{ t("save") }}</v-btn>
       </v-card-actions>
     </v-card>
+    <v-card class="mt-7 ma-3 pa-3">
+      <v-card-title primary-title class="mb-3">
+        <h3 class="headline primary--text">{{ t("changePassword") }}</h3>
+      </v-card-title>
+      <v-card-text>
+        <v-form ref="form" v-model="validPassword">
+          <v-text-field
+            type="password"
+            ref="password"
+            :label="$t('newPassword')"
+            v-model="password1"
+            :rules="password1Rules"
+          >
+          </v-text-field>
+          <v-text-field
+            type="password"
+            :label="$t('confirmNewPassword')"
+            :rules="password2Rules"
+            v-model="password2"
+          >
+          </v-text-field>
+        </v-form>
+      </v-card-text>
+      <v-card-actions>
+        <v-spacer></v-spacer>
+        <v-btn @click="cancel">{{ t("cancel") }}</v-btn>
+        <v-btn @click="resetPassword">{{ t("clear") }}</v-btn>
+        <v-btn @click="submitPassword" :disabled="!validPassword">{{
+          t("save")
+        }}</v-btn>
+      </v-card-actions>
+    </v-card>
   </v-container>
 </template>
-
-<style lang="scss" scoped>
-// .v-card-title {
-//   h3 {
-//     letter-spacing: 1px !important;
-//   }
-// }
-</style>