فهرست منبع

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

SyuanYu 1 سال پیش
والد
کامیت
07010b6acb
54فایلهای تغییر یافته به همراه1687 افزوده شده و 726 حذف شده
  1. 4 1
      backend/app/app/api/api_v1/api.py
  2. 53 115
      backend/app/app/api/api_v1/endpoints/payment.py
  3. 328 0
      backend/app/app/api/api_v1/endpoints/ytviewspayment.py
  4. 17 0
      backend/app/app/core/test_chardet.py
  5. 3 1
      backend/app/app/crud/__init__.py
  6. 35 0
      backend/app/app/crud/crud_ytviews.py
  7. 3 1
      backend/app/app/db/base.py
  8. 4 2
      backend/app/app/models/__init__.py
  9. 4 1
      backend/app/app/models/enum.py
  10. 23 0
      backend/app/app/models/payment.py
  11. 1 0
      backend/app/app/models/user.py
  12. 33 0
      backend/app/app/models/ytviews.py
  13. 1 0
      backend/app/app/schemas/__init__.py
  14. 64 0
      backend/app/app/schemas/ytviews.py
  15. 1 1
      frontend/src/App.vue
  16. 15 5
      frontend/src/api.ts
  17. 0 0
      frontend/src/assets/img/anchor/angela.png
  18. 0 0
      frontend/src/assets/img/anchor/jocelyn.png
  19. 0 0
      frontend/src/assets/img/anchor/peggy.png
  20. 0 0
      frontend/src/assets/img/anchor/summer.png
  21. BIN
      frontend/src/assets/img/icon/discord.png
  22. BIN
      frontend/src/assets/img/icon/facebook.png
  23. BIN
      frontend/src/assets/img/icon/instagram.png
  24. BIN
      frontend/src/assets/img/icon/line.png
  25. BIN
      frontend/src/assets/img/icon/mail.png
  26. BIN
      frontend/src/assets/img/icon/social.png
  27. BIN
      frontend/src/assets/img/icon/twitter.png
  28. BIN
      frontend/src/assets/img/template/舊/鏡面-01.png
  29. 0 0
      frontend/src/assets/img/template/舊/鏡面-01.webp
  30. 0 0
      frontend/src/assets/img/template/舊/鏡面-02.webp
  31. 0 0
      frontend/src/assets/img/template/舊/鏡面-03.webp
  32. 0 0
      frontend/src/assets/img/template/舊/鏡面-04.webp
  33. 0 0
      frontend/src/assets/img/template/舊/鏡面-05.webp
  34. 0 0
      frontend/src/assets/img/template/舊/鏡面-06.webp
  35. BIN
      frontend/src/assets/img/template/鏡面-01.png
  36. BIN
      frontend/src/assets/img/template/鏡面-02.png
  37. BIN
      frontend/src/assets/img/template/鏡面-03.png
  38. BIN
      frontend/src/assets/img/template/鏡面-04.png
  39. 1 0
      frontend/src/components/Navbar.vue
  40. 21 0
      frontend/src/components/imgBox/index.vue
  41. 18 2
      frontend/src/interfaces/index.ts
  42. 4 1
      frontend/src/language/en.json
  43. 4 1
      frontend/src/language/zh.json
  44. 15 6
      frontend/src/router/index.ts
  45. 37 5
      frontend/src/stores/main.ts
  46. 1 1
      frontend/src/utils.ts
  47. 454 0
      frontend/src/views/TestYTViews.vue
  48. 462 0
      frontend/src/views/YTViews.vue
  49. 34 11
      frontend/src/views/main/Main.vue
  50. 1 1
      frontend/src/views/main/Start.vue
  51. 37 117
      frontend/src/views/main/Upload.vue
  52. 7 394
      frontend/src/views/main/admin/TestECPay.vue
  53. 0 58
      frontend/src/views/main/admin/TestStylePreview.vue
  54. 2 2
      frontend/src/views/main/profile/UserProfileEditPassword.vue

+ 4 - 1
backend/app/app/api/api_v1/api.py

@@ -1,6 +1,8 @@
 from fastapi import APIRouter
 from fastapi import APIRouter
 
 
-from app.api.api_v1.endpoints import  login, users, utils, videos, images, reputations, ser_no, payment
+from app.api.api_v1.endpoints import  login, users, utils, videos, images, reputations, ser_no
+from app.api.api_v1.endpoints import payment, ytviewspayment
+
 
 
 api_router = APIRouter()
 api_router = APIRouter()
 api_router.include_router(login.router, tags=["login"])
 api_router.include_router(login.router, tags=["login"])
@@ -10,4 +12,5 @@ api_router.include_router(videos.router, prefix="/videos", tags=["videos"])
 api_router.include_router(images.router, prefix="/images", tags=["iamges"])
 api_router.include_router(images.router, prefix="/images", tags=["iamges"])
 api_router.include_router(reputations.router, prefix="/reputations", tags=["reputations"])
 api_router.include_router(reputations.router, prefix="/reputations", tags=["reputations"])
 api_router.include_router(ser_no.router, prefix="/ser_nos", tags=["serial numbers"])
 api_router.include_router(ser_no.router, prefix="/ser_nos", tags=["serial numbers"])
+api_router.include_router(ytviewspayment.router, prefix="/payment", tags=["yt views payment"])
 api_router.include_router(payment.router, prefix="/payment", tags=["payment"])
 api_router.include_router(payment.router, prefix="/payment", tags=["payment"])

+ 53 - 115
backend/app/app/api/api_v1/endpoints/payment.py

@@ -12,126 +12,54 @@ from app.core.config import settings
 from app.core.ecpay_payment_sdk import ECPayPaymentSdk
 from app.core.ecpay_payment_sdk import ECPayPaymentSdk
 from app.utils import send_new_account_email
 from app.utils import send_new_account_email
 
 
+import requests
+
 router = APIRouter()
 router = APIRouter()
 
 
-@router.post("/ecpayTestPay", response_model=str)
-def ecpay(
+@router.post("/ecpay-result-return")
+def ecpay_return(
     *,
     *,
-    db: Session = Depends(deps.get_db),
-    current_user: models.User = Depends(deps.get_current_active_user),
-    amount: str=Form(...),
+    MerchantID: Optional[str]=Form(None),
+    MerchantTradeNo: Optional[str]=Form(None),
+    StoreID: Optional[str]=Form(None),
+    RtnCode: Optional[int]=Form(None),
+    RtnMsg: Optional[str]=Form(None),
+    TradeNo: Optional[str]=Form(None),
+    TradeAmt: Optional[int]=Form(None),
+    PaymentDate: Optional[str]=Form(None),
+    PaymentType: Optional[str]=Form(None),
+    PaymentTypeChargeFee: Optional[int]=Form(None),
+    TradeDate: Optional[str]=Form(None),
+    SimulatePaid: Optional[int]=Form(None),
+    CustomField1: Optional[str]=Form(None),
+    CustomField2: Optional[str]=Form(None),
+    CustomField3: Optional[str]=Form(None),
+    CustomField4: Optional[str]=Form(None),
+    CheckMacValue: Optional[str]=Form(None),
 ) -> Any:
 ) -> Any:
-    print(amount)
-
-    order_params = {
-        'MerchantTradeNo': datetime.now().strftime("NO%Y%m%d%H%M%S"),
-        'StoreID': '3226141',
-        'MerchantTradeDate': datetime.now().strftime("%Y/%m/%d %H:%M:%S"),
-        'PaymentType': 'aio',
-        'TotalAmount': amount,
-        'TradeDesc': '訂單測試',
-        'ItemName': '商品1#商品2',
-        'ReturnURL': 'https://cloud.choozmo.com/api/v1/payment/ecpayTestReturn',
-        'ChoosePayment': 'ALL',
-        'ClientBackURL': 'http://dev.cloud.choozmo.com:5173/main/admin/test-ecpay',
-        'ItemURL': 'http://dev.cloud.choozmo.com:5173/main/admin/test-ecpay',
-        'Remark': '交易備註',
-        'ChooseSubPayment': '',
-        'OrderResultURL': 'http://dev.cloud.choozmo.com:5173/main/admin/test-ecpay',
-        'NeedExtraPaidInfo': 'Y',
-        'DeviceSource': '',
-        'IgnorePayment': '',
-        'PlatformID': '',
-        'InvoiceMark': 'N',
-        'CustomField1': '',
-        'CustomField2': '',
-        'CustomField3': '',
-        'CustomField4': '',
-        'EncryptType': 1,
-        'Language': '',
-    }
-
-    extend_params_1 = {
-        'ExpireDate': 7,
-        'PaymentInfoURL': 'https://www.ecpay.com.tw/payment_info_url.php',
-        'ClientRedirectURL': '',
-    }
-
-    extend_params_2 = {
-        'StoreExpireDate': 15,
-        'Desc_1': '',
-        'Desc_2': '',
-        'Desc_3': '',
-        'Desc_4': '',
-        'PaymentInfoURL': 'https://www.ecpay.com.tw/payment_info_url.php',
-        'ClientRedirectURL': '',
-    }
-
-    extend_params_3 = {
-        'BindingCard': 0,
-        'MerchantMemberID': '',
-    }
-
-    extend_params_4 = {
-        'Redeem': 'N',
-        'UnionPay': 0,
-    }
-
-    inv_params = {
-        # 'RelateNumber': 'Tea0001', # 特店自訂編號
-        # 'CustomerID': 'TEA_0000001', # 客戶編號
-        # 'CustomerIdentifier': '53348111', # 統一編號
-        # 'CustomerName': '客戶名稱',
-        # 'CustomerAddr': '客戶地址',
-        # 'CustomerPhone': '0912345678', # 客戶手機號碼
-        # 'CustomerEmail': 'abc@ecpay.com.tw',
-        # 'ClearanceMark': '2', # 通關方式
-        # 'TaxType': '1', # 課稅類別
-        # 'CarruerType': '', # 載具類別
-        # 'CarruerNum': '', # 載具編號
-        # 'Donation': '1', # 捐贈註記
-        # 'LoveCode': '168001', # 捐贈碼
-        # 'Print': '1',
-        # 'InvoiceItemName': '測試商品1|測試商品2',
-        # 'InvoiceItemCount': '2|3',
-        # 'InvoiceItemWord': '個|包',
-        # 'InvoiceItemPrice': '35|10',
-        # 'InvoiceItemTaxType': '1|1',
-        # 'InvoiceRemark': '測試商品1的說明|測試商品2的說明',
-        # 'DelayDay': '0', # 延遲天數
-        # 'InvType': '07', # 字軌類別
-    }
-
-    # 建立實體
-    ecpay_payment_sdk = ECPayPaymentSdk(
-        MerchantID='3002607',
-        HashKey='pwFHCqoQZGmho4w6',
-        HashIV='EkRm7iFT261dpevs'
-    )
-
-    # 合併延伸參數
-    order_params.update(extend_params_1)
-    order_params.update(extend_params_2)
-    order_params.update(extend_params_3)
-    order_params.update(extend_params_4)
-
-    # 合併發票參數
-    order_params.update(inv_params)
-
-    try:
-        # 產生綠界訂單所需參數
-        final_order_params = ecpay_payment_sdk.create_order(order_params)
+  #送email
+  print(f"\
+MerchantID: {MerchantID} \n\
+MerchantTradeNo: {MerchantTradeNo}\n\
+StoreID: {StoreID}\n\
+RtnCode: {RtnCode}\n\
+RtnMsg: {RtnMsg}\n\
+TradeNo: {TradeNo}\n\
+TradeAmt: {TradeAmt}\n\
+PaymentDate: {PaymentDate}\n\
+PaymentType: {PaymentType}\n\
+PaymentTypeChargeFee: {PaymentTypeChargeFee}\n\
+TradeDate: {TradeDate}\n\
+SimulatePaid: {SimulatePaid}\n\
+CustomField1: {CustomField1}\n\
+CustomField2: {CustomField2}\n\
+CustomField3: {CustomField3}\n\
+CustomField4: {CustomField4}\n\
+CheckMacValue: {CheckMacValue}\
+  ")
+  return Response(content='1', status_code=status.HTTP_200_OK)
 
 
-        # 產生 html 的 form 格式
-        action_url = 'https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5'  # 測試環境
-        # action_url = 'https://payment.ecpay.com.tw/Cashier/AioCheckOut/V5' # 正式環境
-        html = ecpay_payment_sdk.gen_html_post_form(action_url, final_order_params)
-        print(html)
-        return html
-    except Exception as error:
-        print('An exception happened: ' + str(error))
-        
-@router.post("/ecpayTestReturn")
+@router.post("/ecpay-test-result-return")
 def ecpay_return(
 def ecpay_return(
     *,
     *,
     MerchantID: Optional[str]=Form(None),
     MerchantID: Optional[str]=Form(None),
@@ -152,6 +80,7 @@ def ecpay_return(
     CustomField4: Optional[str]=Form(None),
     CustomField4: Optional[str]=Form(None),
     CheckMacValue: Optional[str]=Form(None),
     CheckMacValue: Optional[str]=Form(None),
 ) -> Any:
 ) -> Any:
+  #送email
   print(f"\
   print(f"\
 MerchantID: {MerchantID} \n\
 MerchantID: {MerchantID} \n\
 MerchantTradeNo: {MerchantTradeNo}\n\
 MerchantTradeNo: {MerchantTradeNo}\n\
@@ -172,3 +101,12 @@ CustomField4: {CustomField4}\n\
 CheckMacValue: {CheckMacValue}\
 CheckMacValue: {CheckMacValue}\
   ")
   ")
   return Response(content='1', status_code=status.HTTP_200_OK)
   return Response(content='1', status_code=status.HTTP_200_OK)
+
+
+@router.get('/check-payment-list')
+def get_payment_list(
+  *,
+  db: Session = Depends(deps.get_db),
+  current_user: models.User = Depends(deps.get_current_active_user)
+):
+  pass

+ 328 - 0
backend/app/app/api/api_v1/endpoints/ytviewspayment.py

@@ -0,0 +1,328 @@
+from typing import Any, List, Optional
+from datetime import datetime
+
+from fastapi import APIRouter, Body, Depends, HTTPException, Form, status, Response
+from fastapi.encoders import jsonable_encoder
+from pydantic.networks import EmailStr, HttpUrl
+from sqlalchemy.orm import Session
+
+import app.crud as crud
+import app.models as models
+import app.schemas as schemas 
+from app.api import deps
+from app.core.config import settings
+from app.core.ecpay_payment_sdk import ECPayPaymentSdk
+from app.utils import send_new_account_email
+from pydantic import BaseModel
+import requests
+from random import choice
+import string
+import json
+
+router = APIRouter()
+
+@router.post('/ytviews-ecpay-test-payment')
+def ecpay_payment(
+  *,
+  db: Session = Depends(deps.get_db),
+  user_data: schemas.YTViewsCreate,
+  lang: str=''
+):  
+    print(user_data.url)
+    MerchantTradeNo = 'test'+datetime.now().strftime("NO%Y%m%d%H%M%S")
+    remark = {'MerchantTradeNo':MerchantTradeNo}
+    remark_string = json.dumps(remark, ensure_ascii=False)
+    ytviews = crud.ytviews.create_with_payment_data(db, 
+                                                        obj_in=user_data, 
+                                                        epayment='ecpay',
+                                                        remark=remark_string)
+    order_params = {
+        'MerchantTradeNo': MerchantTradeNo,
+        'StoreID': 'SaaS',
+        'MerchantTradeDate': datetime.now().strftime("%Y/%m/%d %H:%M:%S"),
+        'PaymentType': 'aio',
+        'TotalAmount': user_data.amount,
+        'TradeDesc': 'YT0.4訂單測試',
+        'ItemName': user_data.item,
+        'ReturnURL': 'https://cloud.choozmo.com/api/v1/payment/ytviews-ecpay-result-return',
+        'ChoosePayment': 'ALL',
+        'ClientBackURL': 'https://cloud.choozmo.com/test-yt-views',
+        'ItemURL': 'https://cloud.choozmo.com/test-yt-views',
+        'Remark': '',
+        'ChooseSubPayment': '',
+        'OrderResultURL': '',
+        'NeedExtraPaidInfo': 'Y',
+        'DeviceSource': '',
+        'IgnorePayment': 'ATM#CVS#BARCODE',
+        'PlatformID': '',
+        'InvoiceMark': 'N',
+        'CustomField1': str(ytviews.id),
+        'CustomField2': user_data.taxID if user_data.taxID else '',
+        'CustomField3': '',
+        'CustomField4': '',
+        'EncryptType': 1,
+        'Language': lang,
+    }
+
+    extend_params_1 = {
+        'ExpireDate': 7,
+        'PaymentInfoURL': 'https://www.ecpay.com.tw/payment_info_url.php',
+        'ClientRedirectURL': '',
+    }
+
+    extend_params_2 = {
+        'StoreExpireDate': 15,
+        'Desc_1': '',
+        'Desc_2': '',
+        'Desc_3': '',
+        'Desc_4': '',
+        'PaymentInfoURL': 'https://www.ecpay.com.tw/payment_info_url.php',
+        'ClientRedirectURL': '',
+    }
+
+    extend_params_3 = {
+        'BindingCard': 0,
+        'MerchantMemberID': '',
+    }
+
+    extend_params_4 = {
+        'Redeem': 'N',
+        'UnionPay': 0,
+    }
+
+    inv_params = {
+        # 'RelateNumber': 'Tea0001', # 特店自訂編號
+        # 'CustomerID': 'TEA_0000001', # 客戶編號
+        # 'CustomerIdentifier': '53348111', # 統一編號
+        # 'CustomerName': '客戶名稱',
+        # 'CustomerAddr': '客戶地址',
+        # 'CustomerPhone': '0912345678', # 客戶手機號碼
+        # 'CustomerEmail': 'abc@ecpay.com.tw',
+        # 'ClearanceMark': '2', # 通關方式
+        # 'TaxType': '1', # 課稅類別
+        # 'CarruerType': '', # 載具類別
+        # 'CarruerNum': '', # 載具編號
+        # 'Donation': '1', # 捐贈註記
+        # 'LoveCode': '168001', # 捐贈碼
+        # 'Print': '1',
+        # 'InvoiceItemName': '測試商品1|測試商品2',
+        # 'InvoiceItemCount': '2|3',
+        # 'InvoiceItemWord': '個|包',
+        # 'InvoiceItemPrice': '35|10',
+        # 'InvoiceItemTaxType': '1|1',
+        # 'InvoiceRemark': '測試商品1的說明|測試商品2的說明',
+        # 'DelayDay': '0', # 延遲天數
+        # 'InvType': '07', # 字軌類別
+    }
+
+    # 建立實體
+    ecpay_payment_sdk = ECPayPaymentSdk(
+        MerchantID='3002607',
+        HashKey='pwFHCqoQZGmho4w6',
+        HashIV='EkRm7iFT261dpevs'
+    )
+
+    # 合併延伸參數
+    order_params.update(extend_params_1)
+    order_params.update(extend_params_2)
+    order_params.update(extend_params_3)
+    order_params.update(extend_params_4)
+
+    # 合併發票參數
+    order_params.update(inv_params)
+    try:
+        # 產生綠界訂單所需參數
+        final_order_params = ecpay_payment_sdk.create_order(order_params)
+
+        # 產生 html 的 form 格式
+        action_url = 'https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5'  # 測試環境
+        # action_url = 'https://payment.ecpay.com.tw/Cashier/AioCheckOut/V5' # 正式環境
+        html = ecpay_payment_sdk.gen_html_post_form(action_url, final_order_params)
+        
+        return html
+    except Exception as error:
+        print('An exception happened: ' + str(error))
+
+@router.post('/ytviews-ecpay-payment')
+def ecpay_payment(
+  *,
+  db: Session = Depends(deps.get_db),
+  user_data: schemas.YTViewsCreate,
+  lang: str
+):
+    print(user_data)
+    numbers = string.digits
+    randomNumber = ''.join(choice(numbers) for _ in range(4))
+    MerchantTradeNo = datetime.now().strftime("%Y%m%d%H%M%SNO")+randomNumber
+    remark = {'MerchantTradeNo':MerchantTradeNo}
+    remark_string = json.dump(remark, ensure_ascii=False)
+    ytviews = crud.ytviews.create_with_payment_data(db, 
+                                                obj_in=user_data, 
+                                                epayment='ecpay',
+                                                remark=remark_string)
+    order_params = {
+        'MerchantTradeNo': MerchantTradeNo,
+        'StoreID': 'SaaS',
+        'MerchantTradeDate': datetime.now().strftime("%Y/%m/%d %H:%M:%S"),
+        'PaymentType': 'aio',
+        'TotalAmount': user_data.amount,
+        'TradeDesc': 'YT0.4訂單',
+        'ItemName': user_data.item,
+        'ReturnURL': 'https://cloud.choozmo.com/api/v1/payment/ytviews-ecpay-result-return',
+        'ChoosePayment': 'ALL',
+        'ClientBackURL': 'https://cloud.choozmo.com/yt-views',
+        'ItemURL': 'https://cloud.choozmo.com/yt-views',
+        'Remark': '',
+        'ChooseSubPayment': '',
+        'OrderResultURL': '',
+        'NeedExtraPaidInfo': 'Y',
+        'DeviceSource': '',
+        'IgnorePayment': 'ATM#CVS#BARCODE',
+        'PlatformID': '',
+        'InvoiceMark': 'N',
+        'CustomField1': str(ytviews.id),
+        'CustomField2': user_data.taxID if user_data.taxID else '',
+        'CustomField3': '',
+        'CustomField4': '',
+        'EncryptType': 1,
+        'Language': lang,
+    }
+
+    extend_params_1 = {
+        'ExpireDate': 7,
+        'PaymentInfoURL': 'https://www.ecpay.com.tw/payment_info_url.php',
+        'ClientRedirectURL': '',
+    }
+
+    extend_params_2 = {
+        'StoreExpireDate': 15,
+        'Desc_1': '',
+        'Desc_2': '',
+        'Desc_3': '',
+        'Desc_4': '',
+        'PaymentInfoURL': 'https://www.ecpay.com.tw/payment_info_url.php',
+        'ClientRedirectURL': '',
+    }
+
+    extend_params_3 = {
+        'BindingCard': 0,
+        'MerchantMemberID': '',
+    }
+
+    extend_params_4 = {
+        'Redeem': 'N',
+        'UnionPay': 0,
+    }
+
+    inv_params = {
+        # 'RelateNumber': 'Tea0001', # 特店自訂編號
+        # 'CustomerID': 'TEA_0000001', # 客戶編號
+        # 'CustomerIdentifier': '53348111', # 統一編號
+        # 'CustomerName': '客戶名稱',
+        # 'CustomerAddr': '客戶地址',
+        # 'CustomerPhone': '0912345678', # 客戶手機號碼
+        # 'CustomerEmail': 'abc@ecpay.com.tw',
+        # 'ClearanceMark': '2', # 通關方式
+        # 'TaxType': '1', # 課稅類別
+        # 'CarruerType': '', # 載具類別
+        # 'CarruerNum': '', # 載具編號
+        # 'Donation': '1', # 捐贈註記
+        # 'LoveCode': '168001', # 捐贈碼
+        # 'Print': '1',
+        # 'InvoiceItemName': '測試商品1|測試商品2',
+        # 'InvoiceItemCount': '2|3',
+        # 'InvoiceItemWord': '個|包',
+        # 'InvoiceItemPrice': '35|10',
+        # 'InvoiceItemTaxType': '1|1',
+        # 'InvoiceRemark': '測試商品1的說明|測試商品2的說明',
+        # 'DelayDay': '0', # 延遲天數
+        # 'InvType': '07', # 字軌類別
+    }
+
+    # 建立實體
+    ecpay_payment_sdk = ECPayPaymentSdk(
+        MerchantID='3226141',
+        HashKey='OhcjDTeXK9PKW9vb',
+        HashIV='AfOmUM06S0bt8KPE'
+    )
+
+    # 合併延伸參數
+    order_params.update(extend_params_1)
+    order_params.update(extend_params_2)
+    order_params.update(extend_params_3)
+    order_params.update(extend_params_4)
+
+    # 合併發票參數
+    order_params.update(inv_params)
+    try:
+        # 產生綠界訂單所需參數
+        final_order_params = ecpay_payment_sdk.create_order(order_params)
+
+        # 產生 html 的 form 格式
+        # action_url = 'https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5'  # 測試環境
+        action_url = 'https://payment.ecpay.com.tw/Cashier/AioCheckOut/V5' # 正式環境
+        html = ecpay_payment_sdk.gen_html_post_form(action_url, final_order_params)
+
+        return html
+    except Exception as error:
+        print('An exception happened: ' + str(error))
+
+
+@router.get('/ytviews-list-all', response_model=List[schemas.YTViews])
+def get_list(
+  *,
+  db: Session = Depends(deps.get_db),
+):
+  ytviews_list = crud.ytviews.get_multi(db)
+  return ytviews_list
+
+@router.post("/ytviews-ecpay-result-return")
+def ecpay_return(
+    *,
+    db: Session = Depends(deps.get_db),
+    MerchantID: Optional[str]=Form(None),
+    MerchantTradeNo: Optional[str]=Form(None),
+    StoreID: Optional[str]=Form(None),
+    RtnCode: Optional[int]=Form(None),
+    RtnMsg: Optional[str]=Form(None),
+    TradeNo: Optional[str]=Form(None),
+    TradeAmt: Optional[int]=Form(None),
+    PaymentDate: Optional[str]=Form(None),
+    PaymentType: Optional[str]=Form(None),
+    PaymentTypeChargeFee: Optional[int]=Form(None),
+    TradeDate: Optional[str]=Form(None),
+    SimulatePaid: Optional[int]=Form(None),
+    CustomField1: Optional[str]=Form(None),
+    CustomField2: Optional[str]=Form(None),
+    CustomField3: Optional[str]=Form(None),
+    CustomField4: Optional[str]=Form(None),
+    CheckMacValue: Optional[str]=Form(None),
+) -> Any:
+  #送email
+  print(f"\
+MerchantID: {MerchantID} \n\
+MerchantTradeNo: {MerchantTradeNo}\n\
+StoreID: {StoreID}\n\
+RtnCode: {RtnCode}\n\
+RtnMsg: {RtnMsg}\n\
+TradeNo: {TradeNo}\n\
+TradeAmt: {TradeAmt}\n\
+PaymentDate: {PaymentDate}\n\
+PaymentType: {PaymentType}\n\
+PaymentTypeChargeFee: {PaymentTypeChargeFee}\n\
+TradeDate: {TradeDate}\n\
+SimulatePaid: {SimulatePaid}\n\
+CustomField1: {CustomField1}\n\
+CustomField2: {CustomField2}\n\
+CustomField3: {CustomField3}\n\
+CustomField4: {CustomField4}\n\
+CheckMacValue: {CheckMacValue}\
+  ")
+  
+  if RtnCode==1:
+    ytviews_id = int(CustomField1)
+    ytviews = crud.ytviews.get(db=db, id=ytviews_id)
+    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)

+ 17 - 0
backend/app/app/core/test_chardet.py

@@ -0,0 +1,17 @@
+from chardet.universaldetector import UniversalDetector
+
+DEFAULT_ENCODING = "utf-8"
+
+def guess_codec(filenames: list) -> str:
+  codec_detector = UniversalDetector()
+  for filename in filenames:
+    codec_detector.feed(filename.encode('cp437'))
+    if codec_detector.done:
+      break
+
+  result = codec_detector.close()
+  encoding = result.get("encoding")
+  return encoding or DEFAULT_ENCODING
+  
+if __name__=="__main__":
+  pass

+ 3 - 1
backend/app/app/crud/__init__.py

@@ -2,7 +2,9 @@ from .crud_user import user
 from .crud_video import video
 from .crud_video import video
 from .crud_article import article
 from .crud_article import article
 from .crud_ser_no import serial_number
 from .crud_ser_no import serial_number
-# For a new basic set of CRUD operations you could just do
+from .crud_ytviews import ytviews
+# For a new basic set of CRUD operations 
+# you could just do
 
 
 # from .base import CRUDBase
 # from .base import CRUDBase
 # from app.models.item import Item
 # from app.models.item import Item

+ 35 - 0
backend/app/app/crud/crud_ytviews.py

@@ -0,0 +1,35 @@
+from typing import List
+
+from fastapi.encoders import jsonable_encoder
+from sqlalchemy.orm import Session
+from sqlalchemy import desc
+from app.crud.base import CRUDBase
+from app.models.ytviews import YTViews
+from app.schemas.ytviews import YTViewsCreate, YTViewsUpdate
+
+from app.utils import random_name
+
+class CRUDYTViews(CRUDBase[YTViews, YTViewsCreate, YTViewsUpdate]):
+    def create_with_payment_data(
+        self, db: Session, *, obj_in: YTViewsCreate, 
+        epayment:str,
+        remark: str
+    ) -> YTViews:
+        
+        obj_in_data = jsonable_encoder(obj_in)
+        db_obj = self.model(**obj_in_data, 
+                            epayment=epayment,
+                            remark=remark)
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+      
+    def get_all_desc(self, db: Session) -> List[YTViews]:
+        return (
+            db.query(self.model)
+            .order_by(desc(YTViews.id))
+            .all()
+        )
+        
+ytviews = CRUDYTViews(YTViews)

+ 3 - 1
backend/app/app/db/base.py

@@ -3,8 +3,10 @@
 from app.db.base_class import Base  # noqa
 from app.db.base_class import Base  # noqa
 from app.models.user import User  # noqa
 from app.models.user import User  # noqa
 from app.models.video import Video
 from app.models.video import Video
-from app.models.enum import Progress, Membership
+from app.models.enum import Progress, Membership, Epayment
 from app.models.character import Character
 from app.models.character import Character
 from app.models.article import Article
 from app.models.article import Article
 from app.models.serial_number import SerialNumber
 from app.models.serial_number import SerialNumber
 from app.models.voice import Voice
 from app.models.voice import Voice
+from app.models.payment import Payment
+from app.models.ytviews import YTViews

+ 4 - 2
backend/app/app/models/__init__.py

@@ -1,6 +1,8 @@
 from .user import User
 from .user import User
 from .video import Video
 from .video import Video
-from .enum import Membership, Progress
+from .enum import Membership, Progress, Epayment
 from .article import Article
 from .article import Article
 from .serial_number import SerialNumber
 from .serial_number import SerialNumber
-from .voice import Voice
+from .voice import Voice
+from .payment import Payment
+from .ytviews import YTViews

+ 4 - 1
backend/app/app/models/enum.py

@@ -9,4 +9,7 @@ class Membership(Base):
   status = Column(String(10), primary_key=True)
   status = Column(String(10), primary_key=True)
 
 
 class Progress(Base):
 class Progress(Base):
-  state = Column(String(10), primary_key=True)
+  state = Column(String(10), primary_key=True)
+  
+class Epayment(Base):
+  name = Column(String(10), primary_key=True)

+ 23 - 0
backend/app/app/models/payment.py

@@ -0,0 +1,23 @@
+from typing import TYPE_CHECKING
+
+from sqlalchemy import Column, ForeignKey, Integer, String, Enum, DateTime, Text
+from sqlalchemy.orm import relationship
+from sqlalchemy.sql import func
+from app.db.base_class import Base
+
+
+if TYPE_CHECKING:
+  from .user import User  # noqa: F401
+
+
+class Payment(Base):
+  id = Column(Integer, primary_key=True, index=True)
+  order_number = Column(String(30), index=True, nullable=False)
+  datetime = Column(DateTime)
+  amount = Column(Integer, nullable=False)
+  payment_state = Column(DateTime(timezone=True), default=func.now())
+  epayment = Column(String(10), 
+                    ForeignKey("epayment.name", ondelete="RESTRICT", onupdate="CASCADE"),
+                    default="ecpay")
+  owner_id = Column(Integer, ForeignKey("user.id"))
+  owner = relationship("User", back_populates="payments")

+ 1 - 0
backend/app/app/models/user.py

@@ -21,3 +21,4 @@ class User(Base):
   is_superuser = Column(Boolean(), default=False)
   is_superuser = Column(Boolean(), default=False)
   videos = relationship("Video", back_populates="owner")
   videos = relationship("Video", back_populates="owner")
   articles = relationship("Article", back_populates="owner")
   articles = relationship("Article", back_populates="owner")
+  payments = relationship("Payment", back_populates="owner")

+ 33 - 0
backend/app/app/models/ytviews.py

@@ -0,0 +1,33 @@
+from typing import TYPE_CHECKING
+
+from sqlalchemy import Column, ForeignKey, Integer, String, Enum, DateTime, Text
+from sqlalchemy.orm import relationship
+from sqlalchemy.sql import func
+from app.db.base_class import Base
+
+
+if TYPE_CHECKING:
+  from .user import User  # noqa: F401
+  from .enum import Progress
+
+class YTViews(Base):
+  id = Column(Integer, primary_key=True, index=True)
+  email = Column(String(50), index=True, nullable=False)
+  name = Column(String(30), index=True)
+  company = Column(String(20))
+  url = Column(String(100), index=True, nullable=False)
+  area = Column(String(20))
+  language = Column(String(20))
+  ages = Column(String(30))
+  target = Column(String(20))
+  theme = Column(String(20))
+  taxID = Column(String(20))
+  item = Column(String(20), nullable=False)
+  amount = Column(Integer, nullable=False)
+  created_datetime = Column(DateTime(timezone=True), default=func.now())
+  payment_state = Column(String(10), default='waiting', nullable=False)
+  epayment = Column(String(10), 
+                    ForeignKey("epayment.name", ondelete="RESTRICT", onupdate="CASCADE"),
+                    default="ecpay")
+  remark = Column(Text)
+  

+ 1 - 0
backend/app/app/schemas/__init__.py

@@ -4,3 +4,4 @@ from .msg import Msg
 from .video import Video, VideoCreate, VideoInDB, VideoUpdate
 from .video import Video, VideoCreate, VideoInDB, VideoUpdate
 from .article import ArticleBase, ArticleCreate, ArticleUpdate
 from .article import ArticleBase, ArticleCreate, ArticleUpdate
 from .serial_number import SerialNumberBase, SerialNumberCreate, SerialNumberUpdate
 from .serial_number import SerialNumberBase, SerialNumberCreate, SerialNumberUpdate
+from .ytviews import YTViewsBase, YTViewsCreate, YTViews

+ 64 - 0
backend/app/app/schemas/ytviews.py

@@ -0,0 +1,64 @@
+from typing import Optional
+
+from pydantic import BaseModel, EmailStr, HttpUrl
+
+# Shared properties
+class YTViewsBase(BaseModel):
+    email: Optional[EmailStr] = None
+    name : Optional[str] = None
+    company: Optional[str] = None
+    url: Optional[HttpUrl] = None
+    area: Optional[str] = None
+    language: Optional[str] = None
+    ages: Optional[str] = None
+    target: Optional[str] = None
+    theme: Optional[str] = None
+    taxID: Optional[str] = None
+    item: Optional[str] = None
+    amount: Optional[str] = None
+    
+# Properties to receive on video creation
+class YTViewsCreate(YTViewsBase):
+    email: EmailStr
+    name : str
+    company: str
+    url: HttpUrl
+    area: str
+    language: str
+    ages: str
+    target: str
+    theme: str
+    item: Optional[str] = None
+    amount: Optional[str] = None
+    
+# Properties to receive on video update
+class YTViewsUpdate(YTViewsBase):
+    pass
+
+# Properties shared by models stored in DB
+class YTViewsInDBBase(YTViewsBase):
+    id: int
+    email: EmailStr
+    name : str
+    company: str
+    url: HttpUrl
+    area: str
+    language: str
+    ages: str
+    target: str
+    theme: str
+    item: str
+    amount: str
+    payment_state: str
+    class Config:
+        orm_mode = True
+
+
+# Properties to return to client
+class YTViews(YTViewsInDBBase):
+    pass
+
+
+# Properties properties stored in DB
+class YTViewsInDB(YTViewsInDBBase):
+    pass

+ 1 - 1
frontend/src/App.vue

@@ -15,7 +15,7 @@ const loggedIn = mainStoreRef.readIsLoggedIn;
 //lifecycle
 //lifecycle
 onMounted(() => {
 onMounted(() => {
   let path = location.pathname;
   let path = location.pathname;
-  if (path !== "/qrcode") {
+  if (path !== "/qrcode" && path !== "/yt-views") {
     mainStore.checkLoggedIn();
     mainStore.checkLoggedIn();
   }
   }
 });
 });

+ 15 - 5
frontend/src/api.ts

@@ -1,6 +1,6 @@
 import axios from "axios";
 import axios from "axios";
 import { apiUrl } from "@/env";
 import { apiUrl } from "@/env";
-import type { IUserProfile, IUserProfileUpdate, IUserProfileCreate, Video, VideoCreate, ArticleCreate, ImageDownload, VideoUploaded } from "@/interfaces";
+import type { IUserProfile, IUserProfileUpdate, IUserProfileCreate, Video, VideoCreate, ArticleCreate, ImageDownload, VideoUploaded, YTViewsUserData } from "@/interfaces";
 
 
 function authHeaders(token: string) {
 function authHeaders(token: string) {
   return {
   return {
@@ -17,7 +17,7 @@ export const api = {
     params.append("password", password);
     params.append("password", password);
     return axios.post(`${apiUrl}/api/v1/login/access-token`, params);
     return axios.post(`${apiUrl}/api/v1/login/access-token`, params);
   },
   },
-  async qrLogInGetToken(username: string, password: string,ser_no: string) {
+  async qrLogInGetToken(username: string, password: string, ser_no: string) {
     const params = new URLSearchParams();
     const params = new URLSearchParams();
     params.append("username", username);
     params.append("username", username);
     params.append("password", password);
     params.append("password", password);
@@ -30,7 +30,7 @@ export const api = {
 
 
     return axios.post(`${apiUrl}/api/v1/login/google/access-token`, params);
     return axios.post(`${apiUrl}/api/v1/login/google/access-token`, params);
   },
   },
-  async qrGoogleLogin(username: string,ser_no: string) {
+  async qrGoogleLogin(username: string, ser_no: string) {
     const params = new URLSearchParams();
     const params = new URLSearchParams();
     params.append("username", username);
     params.append("username", username);
     params.append("password", "google");
     params.append("password", "google");
@@ -81,8 +81,9 @@ export const api = {
   async uploadPlot(token: string, video_data: VideoCreate, file: File) {
   async uploadPlot(token: string, video_data: VideoCreate, file: File) {
     const formData = new FormData();
     const formData = new FormData();
     formData.append("title", video_data.title)
     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_id", video_data.anchor_id)
+    formData.append("style", video_data.style)
+    formData.append("lang", video_data.lang)
     formData.append("upload_file", file)
     formData.append("upload_file", file)
     return axios.post<VideoUploaded>(`${apiUrl}/api/v1/videos/`, formData, authHeaders(token));
     return axios.post<VideoUploaded>(`${apiUrl}/api/v1/videos/`, formData, authHeaders(token));
   },
   },
@@ -140,4 +141,13 @@ export const api = {
     formData.append("amount", amount.toString());
     formData.append("amount", amount.toString());
     return axios.post<string>(`${apiUrl}/api/v1/payment/ecpayTestPay`, formData, authHeaders(token))
     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 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`);
+  },
 };
 };

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


+ 0 - 0
frontend/src/assets/img/anchor/Jocelyn.webp → frontend/src/assets/img/anchor/jocelyn.png


+ 0 - 0
frontend/src/assets/img/anchor/Peggy.webp → frontend/src/assets/img/anchor/peggy.png


+ 0 - 0
frontend/src/assets/img/anchor/Summer.webp → frontend/src/assets/img/anchor/summer.png


BIN
frontend/src/assets/img/icon/discord.png


BIN
frontend/src/assets/img/icon/facebook.png


BIN
frontend/src/assets/img/icon/instagram.png


BIN
frontend/src/assets/img/icon/line.png


BIN
frontend/src/assets/img/icon/mail.png


BIN
frontend/src/assets/img/icon/social.png


BIN
frontend/src/assets/img/icon/twitter.png


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


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

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

+ 21 - 0
frontend/src/components/imgBox/index.vue

@@ -0,0 +1,21 @@
+<script setup lang="ts">
+import {reactive} from "vue"
+
+  const src = defineProps(['src'])
+  console.log(src)
+  const data = reactive({
+    active: false,
+    x: 100,
+    y: 100,
+  })
+</script>
+<template>
+  <img :src="src" class="img-fluid"/>
+</template>
+
+<style>
+.img-fluid {
+  position: absolute;
+}
+</style>
+

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

@@ -51,8 +51,9 @@ export interface Video {
 
 
 export interface VideoCreate {
 export interface VideoCreate {
   title: string;
   title: string;
-  anchor_id: number;
-  lang_id: number;
+  anchor_id: string;
+  style: string;
+  lang: string;
 }
 }
 
 
 export interface VideoUploaded {
 export interface VideoUploaded {
@@ -79,3 +80,18 @@ export interface ImageDownload {
   file_name: string;
   file_name: string;
   stored_file_name: string;
   stored_file_name: string;
 }
 }
+
+export interface YTViewsUserData {
+  item: string,
+  amount: number,
+  email: string;
+  name: string;
+  company: string | null;
+  url: string;
+  area: string;
+  language: string;
+  ages: string | null;
+  target: string;
+  theme: string;
+  taxID: string | null;
+}

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

@@ -63,5 +63,8 @@
     "incorrectUsername": "Incorrect username",
     "incorrectUsername": "Incorrect username",
     "sendingEmail": "Sending password recovery email",
     "sendingEmail": "Sending password recovery email",
     "passwordMailSent": "Password recovery email sent",
     "passwordMailSent": "Password recovery email sent",
-    "acceptZipMessage": "Video processing takes about 5-10 minutes, please be patient"
+    "acceptZipMessage": "Video processing takes about 5-10 minutes, please be patient",
+    "ytViews": "YouTube Views",
+    "contactUs": "Contact Us",
+    "orderDetails": "Order Details"
 }
 }

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

@@ -63,5 +63,8 @@
     "incorrectUsername": "使用者名稱不正確",
     "incorrectUsername": "使用者名稱不正確",
     "sendingEmail": "傳送電子郵件中",
     "sendingEmail": "傳送電子郵件中",
     "passwordMailSent": "重置密碼電子郵件已傳送",
     "passwordMailSent": "重置密碼電子郵件已傳送",
-    "acceptZipMessage": "影片處理需要約 5-10 分鐘,敬請耐心等候"
+    "acceptZipMessage": "影片處理需要約 5-10 分鐘,敬請耐心等候",
+    "ytViews": "網紅加速器",
+    "contactUs": "聯絡我們",
+    "orderDetails": "訂單明細"
 }
 }

+ 15 - 6
frontend/src/router/index.ts

@@ -38,6 +38,16 @@ const router = createRouter({
           name: 'qrcode',
           name: 'qrcode',
           component: () => import(/* webpackChunkName: "reset-password" */ '@/views/Qrcode.vue'),
           component: () => import(/* webpackChunkName: "reset-password" */ '@/views/Qrcode.vue'),
         },
         },
+        {
+          path: 'yt-views',
+          name: 'yt-views',
+          component: () => import('@/views/YTViews.vue'),
+        },
+        {
+          path: 'test-yt-views',
+          name: 'test-yt-views',
+          component: () => import('@/views/TestYTViews.vue'),
+        },
         {
         {
           path: 'main',
           path: 'main',
           name: 'main',
           name: 'main',
@@ -68,6 +78,11 @@ const router = createRouter({
               name: 'progress',
               name: 'progress',
               component: () => import('@/views/main/Progress.vue'),
               component: () => import('@/views/main/Progress.vue'),
             },
             },
+            // {
+            //   path: 'yt-views',
+            //   name: 'yt-views',
+            //   component: () => import('@/views/main/YTViews.vue'),
+            // },
             {
             {
               path: 'profile',
               path: 'profile',
               name: 'profile',
               name: 'profile',
@@ -135,12 +150,6 @@ const router = createRouter({
                   component: () => import(
                   component: () => import(
                     /* webpackChunkName: "main-admin-users-create" */ '@/views/main/admin/TestECPay.vue'),
                     /* webpackChunkName: "main-admin-users-create" */ '@/views/main/admin/TestECPay.vue'),
                 },
                 },
-                {
-                  path: 'test-style-preview',
-                  name: 'test-style-preview',
-                  component: () => import(
-                    /* webpackChunkName: "main-admin-users-create" */ '@/views/main/admin/TestStylePreview.vue'),
-                },
               ],
               ],
             },
             },
           ],
           ],

+ 37 - 5
frontend/src/stores/main.ts

@@ -4,7 +4,7 @@ import { api } from "@/api"
 import router from "@/router"
 import router from "@/router"
 import { getLocalToken, removeLocalToken, saveLocalToken } from "@/utils";
 import { getLocalToken, removeLocalToken, saveLocalToken } from "@/utils";
 import type { AppNotification } from '@/interfaces';
 import type { AppNotification } from '@/interfaces';
-import type { IUserProfile, IUserProfileCreate, IUserProfileUpdate, MainState, Video, VideoCreate, ArticleCreate, Image, ImageDownload, VideoUploaded } from '@/interfaces';
+import type { IUserProfile, IUserProfileCreate, IUserProfileUpdate, MainState, Video, VideoCreate, ArticleCreate, Image, ImageDownload, VideoUploaded, YTViewsUserData } from '@/interfaces';
 import i18n from '@/plugins/i18n'
 import i18n from '@/plugins/i18n'
 import { wsUrl } from "@/env";
 import { wsUrl } from "@/env";
 
 
@@ -454,16 +454,48 @@ export const useMainStore = defineStore("MainStoreId", {
         await mainStore.checkApiError(error);
         await mainStore.checkApiError(error);
       }
       }
     },
     },
-    async ecpayPaymentHTML(amount:number) {
+    async ecpayPaymentHTML(amount: number) {
       const mainStore = useMainStore();
       const mainStore = useMainStore();
       try {
       try {
         const response = await api.ecpayPaymentHTML(mainStore.token, amount)
         const response = await api.ecpayPaymentHTML(mainStore.token, amount)
         if (response) {
         if (response) {
           return response.data;
           return response.data;
-        } 
-      }catch (error) {
+        }
+      } catch (error) {
+        await mainStore.checkApiError(error);
+      }
+    },
+    async YTViewsTestPayment(user_data: YTViewsUserData) {
+      const mainStore = useMainStore();
+      try {
+        const response = (
+          await Promise.all([
+            api.YTViewsTestPayment(user_data),
+            await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 0)),
+          ])
+        );
+        if (response[0]) {
+          return response[0].data;
+        }
+      } catch (error) {
         await mainStore.checkApiError(error);
         await mainStore.checkApiError(error);
       }
       }
-    }
+    },
+    async YTViewsPayment(user_data: YTViewsUserData) {
+      const mainStore = useMainStore();
+      try {
+        const response = (
+          await Promise.all([
+            api.YTViewsPayment(user_data),
+            await new Promise<void>((resolve, _) => setTimeout(() => resolve(), 0)),
+          ])
+        );
+        if (response[0]) {
+          return response[0].data;
+        }
+      } catch (error) {
+        await mainStore.checkApiError(error);
+      }
+    },
   }
   }
 });
 });

+ 1 - 1
frontend/src/utils.ts

@@ -8,7 +8,7 @@ export const removeLocalToken = () => localStorage.removeItem("token");
 import type { Ref } from "vue";
 import type { Ref } from "vue";
 
 
 export const emailRules =  [
 export const emailRules =  [
-  (v:any) => /^[a-z.0-9]+@[a-z.-]+\.[a-z]+$/i.test(v) || 'Must be a valid e-mail.',
+  (v:any) => /^[a-z.0-9]+@[a-z.-]+\.[a-z]+$/i.test(v) || '請輸入有效的電子郵件格式',
 ];
 ];
 
 
 export const required = [
 export const required = [

+ 454 - 0
frontend/src/views/TestYTViews.vue

@@ -0,0 +1,454 @@
+<script setup lang="ts">
+import { ref, reactive, computed } from "vue";
+import type { YTViewsUserData } from "@/interfaces";
+import { useMainStore } from "@/stores/main";
+
+const mainStore = useMainStore();
+const fieldRules = [(value: string) => !!value || "此欄位為必填項目"];
+
+const items = reactive([
+  { title: "100% 真人觀看" },
+  { title: "包含影片設定費" },
+  { title: "開發票" },
+  { title: "包含成效報表" },
+]);
+
+const cardItems = reactive([
+  { view: "5,000", price: "2,700", originalPrice: "3,500", param: 2700 },
+  { view: "10,000", price: "4,400", originalPrice: "5,000", param: 4400 },
+  { view: "30,000", price: "12,400", originalPrice: "13,000", param: 12400 },
+  { view: "50,000", price: "20,400", originalPrice: "21,000", param: 20400 },
+]);
+
+const ageOptions = [
+  { label: "18 - 24 歲" },
+  { label: "25 - 34 歲" },
+  { label: "35 - 44 歲" },
+  { label: "45 - 54 歲" },
+  { label: "55 - 64 歲" },
+  { label: "65 歲以上" },
+];
+
+const objectOptions = [
+  { label: "交通工具與運輸" },
+  { label: "媒體和娛樂" },
+  { label: "家居與園藝" },
+  { label: "新聞與政治" },
+  { label: "旅遊" },
+  { label: "生活型態與興趣" },
+  { label: "科技" },
+  { label: "美容與健康" },
+  { label: "美食與餐飲" },
+  { label: "購物愛好者" },
+  { label: "運動與健身" },
+  { label: "銀行與金融" },
+];
+
+const themeOptions = [
+  { label: "人文與社會" },
+  { label: "保健" },
+  { label: "全球地點(各地區)" },
+  { label: "參考資料(圖書館、博物館與目錄、清單等)" },
+  { label: "圖書與文學" },
+  { label: "家居與園藝" },
+  { label: "寵物與動物" },
+  { label: "工作與教育" },
+  { label: "工商業" },
+  { label: "房地產" },
+  { label: "新聞" },
+  { label: "旅遊與交通" },
+  { label: "汽車與交通工具" },
+  { label: "法律與政府" },
+  { label: "科學" },
+  { label: "網路社群" },
+  { label: "網際網路與電信" },
+  { label: "美容與健身" },
+  { label: "美食佳飲" },
+  { label: "興趣與休閒" },
+  { label: "藝術與娛樂" },
+  { label: "購物" },
+  { label: "遊戲" },
+  { label: "運動" },
+  { label: "金融" },
+  { label: "電腦和電子產品" },
+];
+
+let chooseError = ref(false);
+let assignView = ref();
+let assignPrice = ref();
+
+function activeBtn(view: string, param: number) {
+  assignView.value = view;
+  assignPrice.value = param;
+  chooseError.value = false;
+}
+
+let userData = reactive({
+  email: "",
+  name: "",
+  company: "",
+  url: "",
+  area: "",
+  language: "",
+  ages: [],
+  target: "",
+  theme: "",
+  taxID: "",
+});
+
+// 其他選項
+let target = ref("");
+let theme = ref("");
+let otherTarget = ref("");
+let otherTheme = ref("");
+
+// 檢查必填欄位
+const isSubmitDisabled = computed(() => {
+  return (
+    !userData.email ||
+    !userData.name ||
+    !userData.url ||
+    !userData.area ||
+    !userData.language ||
+    !target.value ||
+    !theme.value
+  );
+});
+
+async function ECPaySubmit() {
+  if (!assignPrice.value) {
+    chooseError.value = true;
+    window.scrollTo({ top: 0, behavior: "smooth" });
+  } else {
+    chooseError.value = false;
+  }
+
+  if (target.value === "其他" && otherTarget.value !== "") {
+    userData.target = otherTarget.value;
+  } else {
+    userData.target = target.value;
+  }
+
+  if (theme.value === "其他" && otherTheme.value !== "") {
+    userData.theme = otherTheme.value;
+  } else {
+    userData.theme = theme.value;
+  }
+
+  let data: YTViewsUserData = {
+    item: `YT0.4-(${assignView.value})`,
+    amount: assignPrice.value,
+    email: userData.email,
+    name: userData.name,
+    company: userData.company,
+    url: userData.url,
+    area: userData.area,
+    language: userData.language,
+    ages: userData.ages.join(","),
+    target: userData.target,
+    theme: userData.theme,
+    taxID: userData.taxID,
+  };
+  const originalHTML = await mainStore.YTViewsTestPayment(data);
+  let formHTML = originalHTML?.replace(
+    '<script type="text/javascript">document.getElementById("data_set").submit();</scr',
+    ""
+  );
+  formHTML = formHTML?.replace("ipt>", "");
+  const payFormElement = document.getElementById("pay-form");
+  payFormElement!.innerHTML = formHTML!;
+  const ecpayForm: HTMLFormElement = <HTMLFormElement>(
+    document.getElementById("data_set")
+  );
+  ecpayForm.submit();
+}
+
+// 資料存進 Google Sheets
+// async function saveData() {
+//   const scriptURL =
+//     "https://script.google.com/macros/s/AKfycbxCcfiOQ695DaxIa3peClqRRTWNj2aUNLbx7ty8U2wKlyU7wreQLioHG-sls5MPKBdlRQ/exec";
+
+//   let formdata = new FormData();
+//   formdata.append("email", userData.email);
+//   formdata.append("name", userData.name);
+//   formdata.append("company", userData.company);
+//   formdata.append("url", userData.url);
+//   formdata.append("area", userData.area);
+//   formdata.append("language", userData.language);
+//   formdata.append("age", userData.age.join("、"));
+//   formdata.append("object", userData.object);
+//   formdata.append("theme", userData.theme);
+//   formdata.append("tax", userData.tax);
+
+//   axios
+//     .post(scriptURL, formdata)
+//     .then(function (response) {
+//       console.log(response.data);
+//     })
+//     .catch(function (error) {
+//       console.log(error);
+//     });
+// }
+</script>
+
+<template>
+  <v-container fluid>
+    <v-card class="ma-3 pa-3">
+      <v-card-title primary-title class="mb-3">
+        <h3 class="headline primary--text">YouTube 觀看數</h3>
+      </v-card-title>
+      <v-card-text>
+        <p class="ms-3">請選擇方案:</p>
+        <v-row no-gutters class="pay-card">
+          <v-col
+            xs="12"
+            sm="6"
+            lg="3"
+            v-for="(item, index) in cardItems"
+            :key="index"
+          >
+            <button @click="activeBtn(item.view, item.param)" class="w-100">
+              <v-card
+                class="ma-3 py-3"
+                :class="{ active: assignPrice === item.param }"
+              >
+                <v-card-title primary-title class="pa-0">
+                  <div class="d-flex flex-column">
+                    <section class="d-flex mx-auto">
+                      <img
+                        width="30"
+                        height="30"
+                        src="@/assets/img/icon/play-button.png"
+                        alt=""
+                        class="me-2"
+                      />
+                      <h5 class="m-0">{{ item.view }}</h5>
+                    </section>
+                    <span class="text-center" style="color: #7c8ba7"
+                      >Views</span
+                    >
+                  </div>
+                  <p class="price">
+                    NT${{ item.price }} <br />
+                    <small>NT${{ item.originalPrice }}</small>
+                  </p>
+                </v-card-title>
+
+                <v-card-text class="d-flex align-center justify-center mt-3">
+                  <ul>
+                    <li
+                      v-for="(item, index) in items"
+                      :key="index"
+                      class="d-flex align-center"
+                    >
+                      <img
+                        width="30"
+                        src="@/assets/img/icon/check.png"
+                        alt=""
+                      />
+                      {{ item.title }}
+                    </li>
+                  </ul>
+                </v-card-text>
+
+                <!-- <v-card-actions class="d-flex justify-center">
+                  <v-btn @click="ECPaySubmit(item.param)"> Buy Now </v-btn>
+                </v-card-actions> -->
+              </v-card>
+            </button>
+          </v-col>
+          <p class="ms-3 error" v-show="chooseError">尚未選擇方案</p>
+        </v-row>
+
+        <v-sheet max-width="500" class="mx-auto mt-10">
+          <v-form @submit.prevent class="ECPay-form">
+            <v-text-field
+              v-model="userData.email"
+              :rules="fieldRules"
+              label="電子郵件"
+              required
+            ></v-text-field>
+            <v-text-field
+              v-model="userData.name"
+              :rules="fieldRules"
+              label="姓名"
+              required
+            ></v-text-field>
+            <v-text-field
+              v-model="userData.company"
+              label="公司 / 所屬產業"
+            ></v-text-field>
+            <v-text-field
+              v-model="userData.url"
+              :rules="fieldRules"
+              label="YouTube 影片網址"
+              required
+            ></v-text-field>
+            <v-text-field
+              v-model="userData.area"
+              :rules="fieldRules"
+              label="影片放送地區(國家 / 縣市)"
+              required
+            ></v-text-field>
+            <v-text-field
+              v-model="userData.language"
+              :rules="fieldRules"
+              label="受眾語言"
+              required
+            ></v-text-field>
+
+            <p class="mt-5">客層(未選擇的話視為全部)</p>
+            <div class="checkbox ms-5">
+              <v-checkbox
+                v-for="option in ageOptions"
+                v-model="userData.ages"
+                :key="option.label"
+                :label="option.label"
+                :value="option.label"
+                color="primary"
+              ></v-checkbox>
+            </div>
+
+            <p class="mt-10 mb-3">
+              目標對象區隔(興趣、習慣)<span class="text-red-darken-1">*</span>
+            </p>
+            <div class="ms-5">
+              <v-radio-group v-model="target">
+                <v-radio
+                  v-for="option in objectOptions"
+                  :key="option.label"
+                  :label="option.label"
+                  :value="option.label"
+                  color="primary"
+                ></v-radio>
+                <v-radio label="其他" value="其他" color="primary"></v-radio>
+                <input v-model="otherTarget" type="text" class="other" />
+              </v-radio-group>
+            </div>
+
+            <p class="mt-5 mb-3">
+              影片主題 <span class="text-red-darken-1">*</span>
+            </p>
+            <div class="ms-5">
+              <v-radio-group v-model="theme">
+                <v-radio
+                  v-for="option in themeOptions"
+                  :key="option.label"
+                  :label="option.label"
+                  :value="option.label"
+                  color="primary"
+                ></v-radio>
+
+                <v-radio label="其他" value="其他" color="primary"></v-radio>
+                <input v-model="otherTheme" type="text" class="other" />
+              </v-radio-group>
+            </div>
+
+            <v-text-field
+              type="number"
+              label="是否需要統編(可填寫統編號碼)"
+              v-model="userData.taxID"
+            ></v-text-field>
+
+            <v-btn
+              @click="ECPaySubmit()"
+              type="submit"
+              block
+              class="mt-2 submit-btn"
+              :disabled="isSubmitDisabled"
+              >送出</v-btn
+            >
+          </v-form>
+        </v-sheet>
+      </v-card-text>
+    </v-card>
+  </v-container>
+
+  <div id="pay-form"></div>
+</template>
+
+<style lang="scss">
+.pay-card {
+  .v-card-title {
+    h5 {
+      font-size: 20px;
+    }
+    span {
+      font-size: 16px;
+      letter-spacing: 1px;
+    }
+    .price {
+      padding: 10px 0;
+      font-size: 26px;
+      font-weight: 600;
+      text-align: center;
+      color: #fff;
+      background-color: var(--main-color);
+      letter-spacing: 2px;
+      small {
+        display: block;
+        margin-top: -3px;
+        font-size: 18px;
+        font-weight: 100;
+        text-decoration: line-through;
+      }
+    }
+  }
+
+  .v-card-text {
+    ul {
+      padding: 0;
+      list-style: none;
+    }
+  }
+
+  .v-card-actions {
+    button {
+      padding: 5px 15px;
+      color: #fff;
+      border-radius: 100px;
+      background-color: var(--main-color);
+      border: 1px solid transparent;
+      &:hover {
+        color: var(--main-color);
+        background-color: #fff;
+        border: 1px solid var(--main-color);
+      }
+    }
+  }
+
+  .active {
+    border: 3px solid var(--main-color);
+  }
+
+  .error {
+    color: #b00020;
+  }
+}
+
+.ECPay-form {
+  font-size: 16px;
+  .checkbox {
+    margin-left: 5px;
+    list-style: none;
+    .v-input {
+      height: 40px;
+    }
+  }
+  .v-input__details {
+    padding-top: 0;
+    padding-bottom: 3px;
+  }
+  .other {
+    margin-left: 40px;
+    border-bottom: 1px solid #333;
+    &:focus-visible {
+      outline: none !important;
+    }
+  }
+  .submit-btn {
+    color: #fff;
+    background-color: var(--main-color);
+  }
+}
+</style>

+ 462 - 0
frontend/src/views/YTViews.vue

@@ -0,0 +1,462 @@
+<script setup lang="ts">
+import { ref, reactive, computed } from "vue";
+import type { YTViewsUserData } from "@/interfaces";
+import { useMainStore } from "@/stores/main";
+import { emailRules } from "@/utils";
+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% 真人觀看" },
+  { title: "包含影片設定費" },
+  { title: "開發票" },
+  { title: "包含成效報表" },
+]);
+
+const cardItems = reactive([
+  { view: "5,000", price: "2,700", originalPrice: "3,500", param: 2700 },
+  { view: "10,000", price: "4,400", originalPrice: "5,000", param: 4400 },
+  { view: "30,000", price: "12,400", originalPrice: "13,000", param: 12400 },
+  { view: "50,000", price: "20,400", originalPrice: "21,000", param: 20400 },
+]);
+
+const ageOptions = [
+  { label: "18 - 24 歲" },
+  { label: "25 - 34 歲" },
+  { label: "35 - 44 歲" },
+  { label: "45 - 54 歲" },
+  { label: "55 - 64 歲" },
+  { label: "65 歲以上" },
+];
+
+const objectOptions = [
+  { label: "交通工具與運輸" },
+  { label: "媒體和娛樂" },
+  { label: "家居與園藝" },
+  { label: "新聞與政治" },
+  { label: "旅遊" },
+  { label: "生活型態與興趣" },
+  { label: "科技" },
+  { label: "美容與健康" },
+  { label: "美食與餐飲" },
+  { label: "購物愛好者" },
+  { label: "運動與健身" },
+  { label: "銀行與金融" },
+];
+
+const themeOptions = [
+  { label: "人文與社會" },
+  { label: "保健" },
+  { label: "全球地點(各地區)" },
+  { label: "參考資料(圖書館、博物館與目錄、清單等)" },
+  { label: "圖書與文學" },
+  { label: "家居與園藝" },
+  { label: "寵物與動物" },
+  { label: "工作與教育" },
+  { label: "工商業" },
+  { label: "房地產" },
+  { label: "新聞" },
+  { label: "旅遊與交通" },
+  { label: "汽車與交通工具" },
+  { label: "法律與政府" },
+  { label: "科學" },
+  { label: "網路社群" },
+  { label: "網際網路與電信" },
+  { label: "美容與健身" },
+  { label: "美食佳飲" },
+  { label: "興趣與休閒" },
+  { label: "藝術與娛樂" },
+  { label: "購物" },
+  { label: "遊戲" },
+  { label: "運動" },
+  { label: "金融" },
+  { label: "電腦和電子產品" },
+];
+
+let chooseError = ref(false);
+let assignView = ref();
+let assignPrice = ref();
+
+function activeBtn(view: string, param: number) {
+  assignView.value = view;
+  assignPrice.value = param;
+  chooseError.value = false;
+}
+
+let userData = reactive({
+  email: "",
+  name: "",
+  company: "",
+  url: "",
+  area: "",
+  language: "",
+  ages: [],
+  target: "",
+  theme: "",
+  taxID: "",
+});
+
+// 其他選項
+let target = ref("");
+let theme = ref("");
+let otherTarget = ref("");
+let otherTheme = ref("");
+
+// 檢查必填欄位
+const isSubmitDisabled = computed(() => {
+  return (
+    !userData.email ||
+    !userData.name ||
+    !userData.url ||
+    !userData.area ||
+    !userData.language ||
+    !target.value ||
+    !theme.value
+  );
+});
+
+async function ECPaySubmit() {
+  if (!assignPrice.value) {
+    chooseError.value = true;
+    window.scrollTo({ top: 0, behavior: "smooth" });
+  } else {
+    chooseError.value = false;
+  }
+
+  if (target.value === "其他" && otherTarget.value !== "") {
+    userData.target = otherTarget.value;
+  } else {
+    userData.target = target.value;
+  }
+
+  if (theme.value === "其他" && otherTheme.value !== "") {
+    userData.theme = otherTheme.value;
+  } else {
+    userData.theme = theme.value;
+  }
+
+  let data: YTViewsUserData = {
+    item: `YT0.4-(${assignView.value})`,
+    amount: assignPrice.value,
+    email: userData.email,
+    name: userData.name,
+    company: userData.company,
+    url: userData.url,
+    area: userData.area,
+    language: userData.language,
+    ages: userData.ages.join(","),
+    target: userData.target,
+    theme: userData.theme,
+    taxID: userData.taxID,
+  };
+  const originalHTML = await mainStore.YTViewsTestPayment(data);
+  let formHTML = originalHTML?.replace(
+    '<script type="text/javascript">document.getElementById("data_set").submit();</scr',
+    ""
+  );
+  formHTML = formHTML?.replace("ipt>", "");
+  const payFormElement = document.getElementById("pay-form");
+  payFormElement!.innerHTML = formHTML!;
+  const ecpayForm: HTMLFormElement = <HTMLFormElement>(
+    document.getElementById("data_set")
+  );
+  console.log(ecpayForm);
+  ecpayForm.submit();
+}
+
+// 資料存進 Google Sheets
+// async function saveData() {
+//   const scriptURL =
+//     "https://script.google.com/macros/s/AKfycbxCcfiOQ695DaxIa3peClqRRTWNj2aUNLbx7ty8U2wKlyU7wreQLioHG-sls5MPKBdlRQ/exec";
+
+//   let formdata = new FormData();
+//   formdata.append("email", userData.email);
+//   formdata.append("name", userData.name);
+//   formdata.append("company", userData.company);
+//   formdata.append("url", userData.url);
+//   formdata.append("area", userData.area);
+//   formdata.append("language", userData.language);
+//   formdata.append("age", userData.age.join("、"));
+//   formdata.append("object", userData.object);
+//   formdata.append("theme", userData.theme);
+//   formdata.append("tax", userData.tax);
+
+//   axios
+//     .post(scriptURL, formdata)
+//     .then(function (response) {
+//       console.log(response.data);
+//     })
+//     .catch(function (error) {
+//       console.log(error);
+//     });
+// }
+</script>
+
+<template>
+  <Navbar />
+
+  <v-container fluid class="mt-16">
+    <v-card class="ma-3 pa-3">
+      <v-card-title primary-title class="mb-3">
+        <h3 class="headline primary--text">YouTube 觀看數</h3>
+      </v-card-title>
+      <v-card-text>
+        <p class="ms-3">請選擇方案:</p>
+        <v-row no-gutters class="pay-card">
+          <v-col
+            xs="12"
+            sm="6"
+            lg="3"
+            v-for="(item, index) in cardItems"
+            :key="index"
+          >
+            <button @click="activeBtn(item.view, item.param)" class="w-100">
+              <v-card
+                class="ma-3 py-3"
+                :class="{ active: assignPrice === item.param }"
+              >
+                <v-card-title primary-title class="pa-0">
+                  <div class="d-flex flex-column">
+                    <section class="d-flex mx-auto">
+                      <img
+                        width="30"
+                        height="30"
+                        src="@/assets/img/icon/play-button.png"
+                        alt=""
+                        class="me-2"
+                      />
+                      <h5 class="m-0">{{ item.view }}</h5>
+                    </section>
+                    <span class="text-center" style="color: #7c8ba7"
+                      >Views</span
+                    >
+                  </div>
+                  <p class="price">
+                    NT${{ item.price }} <br />
+                    <small>NT${{ item.originalPrice }}</small>
+                  </p>
+                </v-card-title>
+
+                <v-card-text class="d-flex align-center justify-center mt-3">
+                  <ul>
+                    <li
+                      v-for="(item, index) in items"
+                      :key="index"
+                      class="d-flex align-center"
+                    >
+                      <img
+                        width="30"
+                        src="@/assets/img/icon/check.png"
+                        alt=""
+                      />
+                      {{ item.title }}
+                    </li>
+                  </ul>
+                </v-card-text>
+
+                <!-- <v-card-actions class="d-flex justify-center">
+                  <v-btn @click="ECPaySubmit(item.param)"> Buy Now </v-btn>
+                </v-card-actions> -->
+              </v-card>
+            </button>
+          </v-col>
+          <p class="ms-3 error" v-show="chooseError">尚未選擇方案</p>
+        </v-row>
+
+        <v-sheet max-width="500" class="mx-auto mt-10">
+          <v-form @submit.prevent class="ECPay-form">
+            <v-text-field
+              v-model="userData.email"
+              :rules="emailRules"
+              label="電子郵件"
+              required
+            ></v-text-field>
+            <v-text-field
+              v-model="userData.name"
+              :rules="fieldRules"
+              label="姓名"
+              required
+            ></v-text-field>
+            <v-text-field
+              v-model="userData.company"
+              label="公司 / 所屬產業"
+            ></v-text-field>
+            <v-text-field
+              v-model="userData.url"
+              :rules="urlRules"
+              label="YouTube 影片網址"
+              required
+            ></v-text-field>
+            <v-text-field
+              v-model="userData.area"
+              :rules="fieldRules"
+              label="影片放送地區(國家 / 縣市)"
+              required
+            ></v-text-field>
+            <v-text-field
+              v-model="userData.language"
+              :rules="fieldRules"
+              label="受眾語言"
+              required
+            ></v-text-field>
+
+            <p class="mt-5">客層(未選擇的話視為全部)</p>
+            <div class="checkbox ms-5">
+              <v-checkbox
+                v-for="option in ageOptions"
+                v-model="userData.ages"
+                :key="option.label"
+                :label="option.label"
+                :value="option.label"
+                color="primary"
+              ></v-checkbox>
+            </div>
+
+            <p class="mt-10 mb-3">
+              目標對象區隔(興趣、習慣)<span class="text-red-darken-1">*</span>
+            </p>
+            <div class="ms-5">
+              <v-radio-group v-model="target">
+                <v-radio
+                  v-for="option in objectOptions"
+                  :key="option.label"
+                  :label="option.label"
+                  :value="option.label"
+                  color="primary"
+                ></v-radio>
+                <v-radio label="其他" value="其他" color="primary"></v-radio>
+                <input v-model="otherTarget" type="text" class="other" />
+              </v-radio-group>
+            </div>
+
+            <p class="mt-5 mb-3">
+              影片主題 <span class="text-red-darken-1">*</span>
+            </p>
+            <div class="ms-5">
+              <v-radio-group v-model="theme">
+                <v-radio
+                  v-for="option in themeOptions"
+                  :key="option.label"
+                  :label="option.label"
+                  :value="option.label"
+                  color="primary"
+                ></v-radio>
+
+                <v-radio label="其他" value="其他" color="primary"></v-radio>
+                <input v-model="otherTheme" type="text" class="other" />
+              </v-radio-group>
+            </div>
+
+            <v-text-field
+              type="number"
+              label="是否需要統編(可填寫統編號碼)"
+              v-model="userData.taxID"
+            ></v-text-field>
+
+            <v-btn
+              @click="ECPaySubmit()"
+              type="submit"
+              block
+              class="mt-2 submit-btn"
+              :disabled="isSubmitDisabled"
+              >送出</v-btn
+            >
+          </v-form>
+        </v-sheet>
+      </v-card-text>
+    </v-card>
+  </v-container>
+
+  <div id="pay-form"></div>
+</template>
+
+<style lang="scss">
+.pay-card {
+  .v-card-title {
+    h5 {
+      font-size: 20px;
+    }
+    span {
+      font-size: 16px;
+      letter-spacing: 1px;
+    }
+    .price {
+      padding: 10px 0;
+      font-size: 26px;
+      font-weight: 600;
+      text-align: center;
+      color: #fff;
+      background-color: var(--main-color);
+      letter-spacing: 2px;
+      small {
+        display: block;
+        margin-top: -3px;
+        font-size: 18px;
+        font-weight: 100;
+        text-decoration: line-through;
+      }
+    }
+  }
+
+  .v-card-text {
+    ul {
+      padding: 0;
+      list-style: none;
+    }
+  }
+
+  .v-card-actions {
+    button {
+      padding: 5px 15px;
+      color: #fff;
+      border-radius: 100px;
+      background-color: var(--main-color);
+      border: 1px solid transparent;
+      &:hover {
+        color: var(--main-color);
+        background-color: #fff;
+        border: 1px solid var(--main-color);
+      }
+    }
+  }
+
+  .active {
+    border: 3px solid var(--main-color);
+  }
+
+  .error {
+    color: #b00020;
+  }
+}
+
+.ECPay-form {
+  font-size: 16px;
+  .checkbox {
+    margin-left: 5px;
+    list-style: none;
+    .v-input {
+      height: 40px;
+    }
+  }
+  .v-input__details {
+    padding-top: 0;
+    padding-bottom: 3px;
+  }
+  .other {
+    margin-left: 40px;
+    border-bottom: 1px solid #333;
+    &:focus-visible {
+      outline: none !important;
+    }
+  }
+  .submit-btn {
+    color: #fff;
+    background-color: var(--main-color);
+  }
+}
+</style>

+ 34 - 11
frontend/src/views/main/Main.vue

@@ -111,24 +111,24 @@ const routeGuardAdmin = async (
             <v-list-item to="/main/progress" prepend-icon="list">
             <v-list-item to="/main/progress" prepend-icon="list">
               <v-list-item-title>{{ t("progress") }}</v-list-item-title>
               <v-list-item-title>{{ t("progress") }}</v-list-item-title>
             </v-list-item>
             </v-list-item>
-            <v-list-item to="/main/make-article" prepend-icon="article">
+            <!-- <v-list-item to="/main/make-article" prepend-icon="article">
               <v-list-item-title>{{ t("article") }}</v-list-item-title>
               <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> -->
+            <!-- <v-list-item to="/main/make-image" prepend-icon="image">
               <v-list-item-title>圖片優化</v-list-item-title>
               <v-list-item-title>圖片優化</v-list-item-title>
-            </v-list-item>
+            </v-list-item> -->
             <!-- <v-list-item to="/main/profile/view" prepend-icon="person">
             <!-- <v-list-item to="/main/profile/view" prepend-icon="person">
               <v-list-item-title>{{ t("userProfile") }}</v-list-item-title>
               <v-list-item-title>{{ t("userProfile") }}</v-list-item-title>
             </v-list-item> -->
             </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> -->
             <v-list-item to="/main/profile/edit" prepend-icon="edit">
             <v-list-item to="/main/profile/edit" prepend-icon="edit">
               <v-list-item-title>{{ t("editProfile") }}</v-list-item-title>
               <v-list-item-title>{{ t("editProfile") }}</v-list-item-title>
             </v-list-item>
             </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-title>{{ t("changePassword") }}</v-list-item-title>
             </v-list-item>
             </v-list-item>
-            <v-list-item to="/main/admin/test-ecpay" prepend-icon="payments">
-              <v-list-item-title>網紅加速器</v-list-item-title>
-            </v-list-item>
           </v-list>
           </v-list>
         </v-sheet>
         </v-sheet>
         <v-divider></v-divider>
         <v-divider></v-divider>
@@ -152,10 +152,7 @@ const routeGuardAdmin = async (
             >
             >
               <v-list-item-title>Test Celery</v-list-item-title>
               <v-list-item-title>Test Celery</v-list-item-title>
             </v-list-item>
             </v-list-item>
-            <v-list-item
-              to="/main/admin/test-ecpay"
-              prepend-icon="payments"
-            >
+            <v-list-item to="/main/admin/test-ecpay" prepend-icon="payments">
               <v-list-item-title>Test ECPay</v-list-item-title>
               <v-list-item-title>Test ECPay</v-list-item-title>
             </v-list-item>
             </v-list-item>
             <v-list-item
             <v-list-item
@@ -239,6 +236,24 @@ const routeGuardAdmin = async (
     </v-main>
     </v-main>
 
 
     <v-footer class="pa-3" app>
     <v-footer class="pa-3" app>
+      <v-spacer></v-spacer>
+      <div class="contact-icon">
+        <a href="https://discord.gg/kHAEcu8T">
+          <img src="@/assets/img/icon/discord.png" alt="discord" />
+        </a>
+        <a href="mailto:service@choozmo.com">
+          <img src="@/assets/img/icon/mail.png" alt="mail" />
+        </a>
+        <a href="https://line.me/R/ti/p/@choozmo?from=page" target="_blank">
+          <img src="@/assets/img/icon/line.png" alt="line" />
+        </a>
+        <a href="https://www.facebook.com/choozmo/" target="_blank">
+          <img src="@/assets/img/icon/facebook.png" alt="facebook" />
+        </a>
+        <a href="https://www.instagram.com/choozmo_cmm/" target="_blank">
+          <img src="@/assets/img/icon/instagram.png" alt="instagram" />
+        </a>
+      </div>
       <v-spacer></v-spacer>
       <v-spacer></v-spacer>
       <span>&copy; ChoozMo</span>
       <span>&copy; ChoozMo</span>
     </v-footer>
     </v-footer>
@@ -262,4 +277,12 @@ const routeGuardAdmin = async (
 .card-title {
 .card-title {
   letter-spacing: 1px !important;
   letter-spacing: 1px !important;
 }
 }
+
+.contact-icon {
+  margin-top: 10px;
+  img {
+    width: 35px;
+    margin: 0 5px;
+  }
+}
 </style>
 </style>

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

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

+ 37 - 117
frontend/src/views/main/Upload.vue

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

+ 7 - 394
frontend/src/views/main/admin/TestECPay.vue

@@ -6,99 +6,6 @@ import axios from "axios";
 import { useMainStore } from "@/stores/main";
 import { useMainStore } from "@/stores/main";
 
 
 const mainStore = useMainStore();
 const mainStore = useMainStore();
-const items = reactive([
-  { title: "100% 真人觀看" },
-  { title: "包含影片設定費" },
-  { title: "開發票" },
-  { title: "包含成效報表" },
-]);
-const cardItems = reactive([
-  { view: "5,000", price: "2,700", originalPrice: "3,500", param: 2700 },
-  { view: "10,000", price: "4,400", originalPrice: "5,000", param: 4400 },
-  { view: "30,000", price: "12,400", originalPrice: "13,000", param: 12400 },
-  { view: "50,000", price: "20,400", originalPrice: "21,000", param: 20400 },
-]);
-
-const ageOptions = [
-  { label: "18 - 24 歲" },
-  { label: "25 - 34 歲" },
-  { label: "35 - 44 歲" },
-  { label: "45 - 54 歲" },
-  { label: "55 - 64 歲" },
-  { label: "65 歲以上" },
-];
-
-const objectOptions = [
-  { label: "交通工具與運輸" },
-  { label: "媒體和娛樂" },
-  { label: "家居與園藝" },
-  { label: "新聞與政治" },
-  { label: "旅遊" },
-  { label: "生活型態與興趣" },
-  { label: "科技" },
-  { label: "美容與健康" },
-  { label: "美食與餐飲" },
-  { label: "購物愛好者" },
-  { label: "運動與健身" },
-  { label: "銀行與金融" },
-];
-
-const themeOptions = [
-  { label: "人文與社會" },
-  { label: "保健" },
-  { label: "全球地點(各地區)" },
-  { label: "參考資料(圖書館、博物館與目錄、清單等)" },
-  { label: "圖書與文學" },
-  { label: "家居與園藝" },
-  { label: "寵物與動物" },
-  { label: "工作與教育" },
-  { label: "工商業" },
-  { label: "房地產" },
-  { label: "新聞" },
-  { label: "旅遊與交通" },
-  { label: "汽車與交通工具" },
-  { label: "法律與政府" },
-  { label: "科學" },
-  { label: "網路社群" },
-  { label: "網際網路與電信" },
-  { label: "美容與健身" },
-  { label: "美食佳飲" },
-  { label: "興趣與休閒" },
-  { label: "藝術與娛樂" },
-  { label: "購物" },
-  { label: "遊戲" },
-  { label: "運動" },
-  { label: "金融" },
-  { label: "電腦和電子產品" },
-];
-
-let chooseError = ref(false);
-let selectedBtn = ref();
-
-function activeBtn(param: any) {
-  selectedBtn.value = param;
-  chooseError.value = false;
-}
-
-let userData = reactive({
-  email: "",
-  name: "",
-  company: "",
-  url: "",
-  area: "",
-  language: "",
-  age: [],
-  object: "",
-  theme: "",
-  tax: "",
-});
-
-// 其他選項
-let object = ref("");
-let theme = ref("");
-let otherObject = ref("");
-let otherTheme = ref("");
-
 async function ECPaySubmit() {
 async function ECPaySubmit() {
   /*
   /*
   const timestamp = Date.now()
   const timestamp = Date.now()
@@ -141,29 +48,7 @@ async function ECPaySubmit() {
   return axios.post("https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5", formData)
   return axios.post("https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5", formData)
   */
   */
 
 
-  // 是否選擇方案
-  if (!selectedBtn.value) {
-    chooseError.value = true;
-    window.scrollTo({ top: 0, behavior: "smooth" }); // 捲動至上方
-  } else {
-    chooseError.value = false;
-  }
-
-  if (object.value === "其他" && otherObject.value !== "") {
-    userData.object = otherObject.value;
-  } else {
-    userData.object = object.value;
-  }
-
-  if (theme.value === "其他" && otherTheme.value !== "") {
-    userData.theme = otherTheme.value;
-  } else {
-    userData.theme = theme.value;
-  }
-
-  await saveData();
-
-  const originalHTML = await mainStore.ecpayPaymentHTML(selectedBtn.value);
+  const originalHTML = await mainStore.ecpayPaymentHTML(500);
   let formHTML = originalHTML?.replace(
   let formHTML = originalHTML?.replace(
     '<script type="text/javascript">document.getElementById("data_set").submit();</scr',
     '<script type="text/javascript">document.getElementById("data_set").submit();</scr',
     ""
     ""
@@ -177,292 +62,20 @@ async function ECPaySubmit() {
   console.log(ecpayForm);
   console.log(ecpayForm);
   ecpayForm.submit();
   ecpayForm.submit();
 }
 }
-
-// 資料存進 Google Sheets
-async function saveData() {
-  const scriptURL =
-    "https://script.google.com/macros/s/AKfycbxCcfiOQ695DaxIa3peClqRRTWNj2aUNLbx7ty8U2wKlyU7wreQLioHG-sls5MPKBdlRQ/exec";
-
-  let formdata = new FormData();
-  formdata.append("email", userData.email);
-  formdata.append("name", userData.name);
-  formdata.append("company", userData.company);
-  formdata.append("url", userData.url);
-  formdata.append("area", userData.area);
-  formdata.append("language", userData.language);
-  formdata.append("age", userData.age.join("、"));
-  formdata.append("object", userData.object);
-  formdata.append("theme", userData.theme);
-  formdata.append("tax", userData.tax);
-
-  axios
-    .post(scriptURL, formdata)
-    .then(function (response) {
-      console.log(response.data);
-    })
-    .catch(function (error) {
-      console.log(error);
-    });
-}
 </script>
 </script>
 
 
 <template>
 <template>
   <v-container fluid>
   <v-container fluid>
     <v-card class="ma-3 pa-3">
     <v-card class="ma-3 pa-3">
-      <v-card-title primary-title class="mb-3">
-        <h3 class="headline primary--text">YouTube 觀看數</h3>
+      <v-card-title primary-title>
+        <div class="headline primary--text">Test ECPay</div>
       </v-card-title>
       </v-card-title>
-      <v-card-text>
-        <p class="ms-3">請選擇方案:</p>
-        <v-row no-gutters class="pay-card">
-          <v-col
-            xs="12"
-            sm="6"
-            lg="3"
-            v-for="(item, index) in cardItems"
-            :key="index"
-          >
-            <button @click="activeBtn(item.param)" class="w-100">
-              <v-card
-                class="ma-3 py-3"
-                :class="{ active: selectedBtn === item.param }"
-              >
-                <v-card-title primary-title class="pa-0">
-                  <div class="d-flex flex-column">
-                    <section class="d-flex mx-auto">
-                      <img
-                        width="30"
-                        height="30"
-                        src="@/assets/img/icon/play-button.png"
-                        alt=""
-                        class="me-2"
-                      />
-                      <h5 class="m-0">{{ item.view }}</h5>
-                    </section>
-                    <span class="text-center" style="color: #7c8ba7"
-                      >Views</span
-                    >
-                  </div>
-                  <p class="price">
-                    NT${{ item.price }} <br />
-                    <small>NT${{ item.originalPrice }}</small>
-                  </p>
-                </v-card-title>
-
-                <v-card-text class="d-flex align-center justify-center mt-3">
-                  <ul>
-                    <li
-                      v-for="(item, index) in items"
-                      :key="index"
-                      class="d-flex align-center"
-                    >
-                      <img
-                        width="30"
-                        src="@/assets/img/icon/check.png"
-                        alt=""
-                      />
-                      {{ item.title }}
-                    </li>
-                  </ul>
-                </v-card-text>
-
-                <!-- <v-card-actions class="d-flex justify-center">
-                  <v-btn @click="ECPaySubmit(item.param)"> Buy Now </v-btn>
-                </v-card-actions> -->
-              </v-card>
-            </button>
-          </v-col>
-          <p class="ms-3 error" v-show="chooseError">尚未選擇方案</p>
-        </v-row>
-
-        <v-sheet max-width="500" class="mx-auto mt-10">
-          <v-form @submit.prevent class="ECPay-form">
-            <v-text-field
-              v-model="userData.email"
-              :rules="[(v) => !!v || '請輸入您的電子郵件']"
-              label="電子郵件"
-              required
-            ></v-text-field>
-            <v-text-field
-              v-model="userData.name"
-              :rules="[(v) => !!v || '請輸入您的姓名']"
-              label="姓名"
-              required
-            ></v-text-field>
-            <v-text-field
-              v-model="userData.company"
-              label="公司 / 所屬產業"
-            ></v-text-field>
-            <v-text-field
-              v-model="userData.url"
-              :rules="[(v) => !!v || '請輸入 YouTube 影片網址']"
-              label="YouTube 影片網址"
-              required
-            ></v-text-field>
-            <v-text-field
-              v-model="userData.area"
-              :rules="[(v) => !!v || '請輸入影片放送地區']"
-              label="影片放送地區(國家 / 縣市)"
-              required
-            ></v-text-field>
-            <v-text-field
-              v-model="userData.language"
-              :rules="[(v) => !!v || '請輸入受眾語言']"
-              label="受眾語言"
-              required
-            ></v-text-field>
-
-            <p class="mt-5">客層(未選擇的話視為全部)</p>
-            <div class="checkbox ms-5">
-              <v-checkbox
-                v-for="option in ageOptions"
-                v-model="userData.age"
-                :key="option.label"
-                :label="option.label"
-                :value="option.label"
-                color="primary"
-              ></v-checkbox>
-            </div>
-
-            <p class="mt-10 mb-3">
-              目標對象區隔(興趣、習慣)<span class="text-red-darken-1">*</span>
-            </p>
-            <div class="ms-5">
-              <v-radio-group v-model="object">
-                <v-radio
-                  v-for="option in objectOptions"
-                  :key="option.label"
-                  :label="option.label"
-                  :value="option.label"
-                  color="primary"
-                ></v-radio>
-                <v-radio label="其他" value="其他" color="primary"></v-radio>
-                <input v-model="otherObject" type="text" class="other" />
-              </v-radio-group>
-            </div>
-
-            <p class="mt-5 mb-3">
-              影片主題 <span class="text-red-darken-1">*</span>
-            </p>
-            <div class="ms-5">
-              <v-radio-group v-model="theme">
-                <v-radio
-                  v-for="option in themeOptions"
-                  :key="option.label"
-                  :label="option.label"
-                  :value="option.label"
-                  color="primary"
-                ></v-radio>
-
-                <v-radio label="其他" value="其他" color="primary"></v-radio>
-                <input v-model="otherTheme" type="text" class="other" />
-              </v-radio-group>
-            </div>
-
-            <v-text-field
-              type="number"
-              label="是否需要統編(可填寫統編號碼)"
-              v-model="userData.tax"
-            ></v-text-field>
-
-            <v-btn
-              @click="ECPaySubmit()"
-              type="submit"
-              block
-              class="mt-2 submit-btn"
-              >送出</v-btn
-            >
-          </v-form>
-        </v-sheet>
-      </v-card-text>
+      <v-card-actions>
+        <v-spacer></v-spacer>
+        <v-btn @click="ECPaySubmit"> Send </v-btn>
+      </v-card-actions>
     </v-card>
     </v-card>
   </v-container>
   </v-container>
 
 
   <div id="pay-form"></div>
   <div id="pay-form"></div>
 </template>
 </template>
-
-<style lang="scss">
-.pay-card {
-  .v-card-title {
-    h5 {
-      font-size: 20px;
-    }
-    span {
-      font-size: 16px;
-      letter-spacing: 1px;
-    }
-    .price {
-      padding: 10px 0;
-      font-size: 26px;
-      font-weight: 600;
-      text-align: center;
-      color: #fff;
-      background-color: var(--main-color);
-      letter-spacing: 2px;
-      small {
-        display: block;
-        margin-top: -3px;
-        font-size: 18px;
-        font-weight: 100;
-        text-decoration: line-through;
-      }
-    }
-  }
-
-  .v-card-text {
-    ul {
-      padding: 0;
-      list-style: none;
-    }
-  }
-
-  .v-card-actions {
-    button {
-      padding: 5px 15px;
-      color: #fff;
-      border-radius: 100px;
-      background-color: var(--main-color);
-      border: 1px solid transparent;
-      &:hover {
-        color: var(--main-color);
-        background-color: #fff;
-        border: 1px solid var(--main-color);
-      }
-    }
-  }
-
-  .active {
-    border: 3px solid var(--main-color);
-  }
-
-  .error {
-    color: #b00020;
-  }
-}
-
-.ECPay-form {
-  font-size: 16px;
-  .checkbox {
-    margin-left: 5px;
-    list-style: none;
-    .v-input {
-      height: 40px;
-    }
-  }
-  .v-input__details {
-    padding-top: 0;
-    padding-bottom: 3px;
-  }
-  .other {
-    margin-left: 40px;
-    border-bottom: 1px solid #333;
-    &:focus-visible {
-      outline: none !important;
-    }
-  }
-  .submit-btn {
-    color: #fff;
-    background-color: var(--main-color);
-  }
-}
-</style>

+ 0 - 58
frontend/src/views/main/admin/TestStylePreview.vue

@@ -1,58 +0,0 @@
-<script setup lang="ts">
-import { ref, reactive, watch, computed } from "vue";
-
-const inputFiles = ref();
-const preview_list:any = ref([]);
-const image_list:any = ref([]);
-
-
-function uploadImage() {
-  console.log(inputFiles.value[0])
-  const reader = new FileReader();
-  
-  reader.onload = function () {
-    preview_list.value.push(reader.result)
-  }
-  image_list.value.push(inputFiles.value[0]);
-  reader.readAsDataURL(inputFiles.value[0]);
-  console.log('finish uploadImage')
-  console.log(image_list.value[0].name)
-};
-</script>
-<template>
-  <div class="border p-2 mt-3">
-    <p>Style Preview</p>
-  </div>
-  
-  
-
-  <v-file-input
-    v-model="inputFiles"
-    accept="image/*, video/*"
-    :label="$t('fileInput')"
-    prepend-icon="add_photo_alternate"
-    @update:model-value="uploadImage"
-  ></v-file-input>
-  <v-sheet  class="canvas d-flex">
-    <template v-if="preview_list.length">
-      <div v-for="item in preview_list">
-        <img :src="item" class="img-fluid" />
-      </div>
-    </template>
-  </v-sheet> 
-  <v-sheet class="img-list"  color="grey-lighten-3">
-    <template v-if="image_list.length">
-      <div v-for="item in image_list">
-        <p class="mb-0">file name: {{ item.name }}</p>
-      </div>
-    </template>
-  </v-sheet>
-
-
-</template>
-<style lang="scss">
-.img-fluid {
-  
-}
-</style>
-

+ 2 - 2
frontend/src/views/main/profile/UserProfileEditPassword.vue

@@ -60,14 +60,14 @@ async function submit() {
           <v-text-field
           <v-text-field
             type="password"
             type="password"
             ref="password"
             ref="password"
-            :label="$t('password')"
+            :label="$t('newPassword')"
             v-model="password1"
             v-model="password1"
             :rules="password1Rules"
             :rules="password1Rules"
           >
           >
           </v-text-field>
           </v-text-field>
           <v-text-field
           <v-text-field
             type="password"
             type="password"
-            :label="$t('confirmPassword')"
+            :label="$t('confirmNewPassword')"
             :rules="password2Rules"
             :rules="password2Rules"
             v-model="password2"
             v-model="password2"
           >
           >