소스 검색

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 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.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(reputations.router, prefix="/reputations", tags=["reputations"])
 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"])

+ 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.utils import send_new_account_email
 
+import requests
+
 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:
-    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(
     *,
     MerchantID: Optional[str]=Form(None),
@@ -152,6 +80,7 @@ def ecpay_return(
     CustomField4: Optional[str]=Form(None),
     CheckMacValue: Optional[str]=Form(None),
 ) -> Any:
+  #送email
   print(f"\
 MerchantID: {MerchantID} \n\
 MerchantTradeNo: {MerchantTradeNo}\n\
@@ -172,3 +101,12 @@ CustomField4: {CustomField4}\n\
 CheckMacValue: {CheckMacValue}\
   ")
   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_article import article
 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 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.models.user import User  # noqa
 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.article import Article
 from app.models.serial_number import SerialNumber
 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 .video import Video
-from .enum import Membership, Progress
+from .enum import Membership, Progress, Epayment
 from .article import Article
 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)
 
 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)
   videos = relationship("Video", 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 .article import ArticleBase, ArticleCreate, ArticleUpdate
 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
 onMounted(() => {
   let path = location.pathname;
-  if (path !== "/qrcode") {
+  if (path !== "/qrcode" && path !== "/yt-views") {
     mainStore.checkLoggedIn();
   }
 });

+ 15 - 5
frontend/src/api.ts

@@ -1,6 +1,6 @@
 import axios from "axios";
 import { apiUrl } from "@/env";
-import type { IUserProfile, IUserProfileUpdate, IUserProfileCreate, Video, VideoCreate, ArticleCreate, ImageDownload, VideoUploaded } from "@/interfaces";
+import type { IUserProfile, IUserProfileUpdate, IUserProfileCreate, Video, VideoCreate, ArticleCreate, ImageDownload, VideoUploaded, YTViewsUserData } from "@/interfaces";
 
 function authHeaders(token: string) {
   return {
@@ -17,7 +17,7 @@ export const api = {
     params.append("password", password);
     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();
     params.append("username", username);
     params.append("password", password);
@@ -30,7 +30,7 @@ export const api = {
 
     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();
     params.append("username", username);
     params.append("password", "google");
@@ -81,8 +81,9 @@ export const api = {
   async uploadPlot(token: string, video_data: VideoCreate, file: File) {
     const formData = new FormData();
     formData.append("title", video_data.title)
-    formData.append("anchor_id", video_data.anchor_id.toString())
-    formData.append("lang_id", video_data.lang_id.toString())
+    formData.append("anchor_id", video_data.anchor_id)
+    formData.append("style", video_data.style)
+    formData.append("lang", video_data.lang)
     formData.append("upload_file", file)
     return axios.post<VideoUploaded>(`${apiUrl}/api/v1/videos/`, formData, authHeaders(token));
   },
@@ -140,4 +141,13 @@ export const api = {
     formData.append("amount", amount.toString());
     return axios.post<string>(`${apiUrl}/api/v1/payment/ecpayTestPay`, formData, authHeaders(token))
   },
+  async YTViewsPayment(user_data: YTViewsUserData) {
+    return axios.post<string>(`${apiUrl}/api/v1/payment/ytviews-ecpay-payment`, user_data);
+  },
+  async 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([
   { title: "login", link: "/login" },
   { title: "register", link: "/signup" },
+  { title: "ytViews", link: "/yt-views" },
   // { 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 {
   title: string;
-  anchor_id: number;
-  lang_id: number;
+  anchor_id: string;
+  style: string;
+  lang: string;
 }
 
 export interface VideoUploaded {
@@ -79,3 +80,18 @@ export interface ImageDownload {
   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",
     "sendingEmail": "Sending password recovery email",
     "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": "使用者名稱不正確",
     "sendingEmail": "傳送電子郵件中",
     "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',
           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',
           name: 'main',
@@ -68,6 +78,11 @@ const router = createRouter({
               name: 'progress',
               component: () => import('@/views/main/Progress.vue'),
             },
+            // {
+            //   path: 'yt-views',
+            //   name: 'yt-views',
+            //   component: () => import('@/views/main/YTViews.vue'),
+            // },
             {
               path: 'profile',
               name: 'profile',
@@ -135,12 +150,6 @@ const router = createRouter({
                   component: () => import(
                     /* 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 { getLocalToken, removeLocalToken, saveLocalToken } from "@/utils";
 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 { wsUrl } from "@/env";
 
@@ -454,16 +454,48 @@ export const useMainStore = defineStore("MainStoreId", {
         await mainStore.checkApiError(error);
       }
     },
-    async ecpayPaymentHTML(amount:number) {
+    async ecpayPaymentHTML(amount: number) {
       const mainStore = useMainStore();
       try {
         const response = await api.ecpayPaymentHTML(mainStore.token, amount)
         if (response) {
           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);
       }
-    }
+    },
+    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";
 
 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 = [

+ 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-title>{{ t("progress") }}</v-list-item-title>
             </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>
-            <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>
+            </v-list-item> -->
             <!-- <v-list-item to="/main/profile/view" prepend-icon="person">
               <v-list-item-title>{{ t("userProfile") }}</v-list-item-title>
             </v-list-item> -->
+            <!-- <v-list-item to="/main/yt-views" prepend-icon="ondemand_video">
+              <v-list-item-title>網紅加速器</v-list-item-title>
+            </v-list-item> -->
             <v-list-item to="/main/profile/edit" prepend-icon="edit">
               <v-list-item-title>{{ t("editProfile") }}</v-list-item-title>
             </v-list-item>
             <v-list-item to="/main/profile/password" prepend-icon="key">
               <v-list-item-title>{{ t("changePassword") }}</v-list-item-title>
             </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-sheet>
         <v-divider></v-divider>
@@ -152,10 +152,7 @@ const routeGuardAdmin = async (
             >
               <v-list-item-title>Test Celery</v-list-item-title>
             </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>
             <v-list-item
@@ -239,6 +236,24 @@ const routeGuardAdmin = async (
     </v-main>
 
     <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>
       <span>&copy; ChoozMo</span>
     </v-footer>
@@ -262,4 +277,12 @@ const routeGuardAdmin = async (
 .card-title {
   letter-spacing: 1px !important;
 }
+
+.contact-icon {
+  margin-top: 10px;
+  img {
+    width: 35px;
+    margin: 0 5px;
+  }
+}
 </style>

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

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

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

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

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

@@ -6,99 +6,6 @@ import axios from "axios";
 import { useMainStore } from "@/stores/main";
 
 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() {
   /*
   const timestamp = Date.now()
@@ -141,29 +48,7 @@ async function ECPaySubmit() {
   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(
     '<script type="text/javascript">document.getElementById("data_set").submit();</scr',
     ""
@@ -177,292 +62,20 @@ async function ECPaySubmit() {
   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>
   <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 primary-title>
+        <div class="headline primary--text">Test ECPay</div>
       </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-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>

+ 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
             type="password"
             ref="password"
-            :label="$t('password')"
+            :label="$t('newPassword')"
             v-model="password1"
             :rules="password1Rules"
           >
           </v-text-field>
           <v-text-field
             type="password"
-            :label="$t('confirmPassword')"
+            :label="$t('confirmNewPassword')"
             :rules="password2Rules"
             v-model="password2"
           >