Преглед изворни кода

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

tomoya пре 1 година
родитељ
комит
bbbfca40d5
34 измењених фајлова са 1613 додато и 596 уклоњено
  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. 3 1
      backend/app/app/crud/__init__.py
  5. 35 0
      backend/app/app/crud/crud_ytviews.py
  6. 3 1
      backend/app/app/db/base.py
  7. 4 2
      backend/app/app/models/__init__.py
  8. 4 1
      backend/app/app/models/enum.py
  9. 23 0
      backend/app/app/models/payment.py
  10. 1 0
      backend/app/app/models/user.py
  11. 33 0
      backend/app/app/models/ytviews.py
  12. 1 0
      backend/app/app/schemas/__init__.py
  13. 64 0
      backend/app/app/schemas/ytviews.py
  14. 9 3
      frontend/src/api.ts
  15. BIN
      frontend/src/assets/img/icon/discord.png
  16. BIN
      frontend/src/assets/img/icon/facebook.png
  17. BIN
      frontend/src/assets/img/icon/instagram.png
  18. BIN
      frontend/src/assets/img/icon/line.png
  19. BIN
      frontend/src/assets/img/icon/mail.png
  20. BIN
      frontend/src/assets/img/icon/social.png
  21. BIN
      frontend/src/assets/img/icon/twitter.png
  22. 1 0
      frontend/src/components/Navbar.vue
  23. 21 0
      frontend/src/components/imgBox/index.vue
  24. 15 0
      frontend/src/interfaces/index.ts
  25. 2 1
      frontend/src/language/en.json
  26. 2 1
      frontend/src/language/zh.json
  27. 15 0
      frontend/src/router/index.ts
  28. 37 5
      frontend/src/stores/main.ts
  29. 454 0
      frontend/src/views/TestYTViews.vue
  30. 458 0
      frontend/src/views/YTViews.vue
  31. 34 11
      frontend/src/views/main/Main.vue
  32. 7 394
      frontend/src/views/main/admin/TestECPay.vue
  33. 0 58
      frontend/src/views/main/admin/TestStylePreview.vue
  34. 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.comtest-yt-views/test-yt-views',
+        'ChoosePayment': 'ALL',
+        'ClientBackURL': 'https://cloud.choozmo.comtest-yt-views/test-yt-views',
+        'ItemURL': 'https://cloud.choozmo.comtest-yt-views/test-yt-views',
+        'Remark': '',
+        'ChooseSubPayment': '',
+        'OrderResultURL': 'https://cloud.choozmo.comtest-yt-views/test-yt-views',
+        '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.comtest-yt-views/yt-views',
+        'ChoosePayment': 'ALL',
+        'ClientBackURL': 'https://cloud.choozmo.comtest-yt-views/yt-views',
+        'ItemURL': 'https://cloud.choozmo.comtest-yt-views/yt-views',
+        'Remark': '',
+        'ChooseSubPayment': '',
+        'OrderResultURL': 'https://cloud.choozmo.comtest-yt-views/yt-views',
+        '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.post('/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)

+ 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:
+        print("in create_with_payment_data")
+        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

+ 9 - 3
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");
@@ -140,4 +140,10 @@ 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);
+  },
 };
 };

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


+ 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>
+

+ 15 - 0
frontend/src/interfaces/index.ts

@@ -79,3 +79,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;
+}

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

@@ -63,5 +63,6 @@
     "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"
 }
 }

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

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

+ 15 - 0
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',

+ 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);
+      }
+    },
   }
   }
 });
 });

+ 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>

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

@@ -0,0 +1,458 @@
+<script setup lang="ts">
+import { ref, reactive, computed } from "vue";
+import type { YTViewsUserData } from "@/interfaces";
+import { useMainStore } from "@/stores/main";
+import Navbar from "@/components/Navbar.vue";
+
+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")
+  );
+  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="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>

+ 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>

+ 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"
           >
           >