diff --git a/src/assets/images/auth/bg.jpg b/src/assets/images/auth/bg.jpg new file mode 100644 index 0000000..849407f Binary files /dev/null and b/src/assets/images/auth/bg.jpg differ diff --git a/src/assets/images/auth/bg.png b/src/assets/images/auth/bg.png deleted file mode 100644 index 63667e7..0000000 Binary files a/src/assets/images/auth/bg.png and /dev/null differ diff --git a/src/assets/images/pay/asia_pay.tsx b/src/assets/images/pay/asia_pay.tsx new file mode 100644 index 0000000..18fa000 --- /dev/null +++ b/src/assets/images/pay/asia_pay.tsx @@ -0,0 +1,37 @@ +const AsiaPayLogo = () => ( + + + + + + + + + +) + +export default AsiaPayLogo \ No newline at end of file diff --git a/src/assets/images/pay/flywire.tsx b/src/assets/images/pay/flywire.tsx new file mode 100644 index 0000000..f8cf70b --- /dev/null +++ b/src/assets/images/pay/flywire.tsx @@ -0,0 +1,19 @@ +const FlywireLogo = () => ( + + + +) + +export default FlywireLogo \ No newline at end of file diff --git a/src/assets/images/pay/success.tsx b/src/assets/images/pay/success.tsx new file mode 100644 index 0000000..9b2ad70 --- /dev/null +++ b/src/assets/images/pay/success.tsx @@ -0,0 +1,7 @@ +export const SuccessIcon = () => ( + + + ) \ No newline at end of file diff --git a/src/assets/index.less b/src/assets/index.less index 5a93a4c..08414e4 100644 --- a/src/assets/index.less +++ b/src/assets/index.less @@ -37,7 +37,9 @@ .space-between { justify-content: space-between; } - +.space-center{ + justify-content: center; +} .align-center { display: flex; align-items: center; @@ -48,9 +50,22 @@ width: 1400px; margin: auto; } +.main-bg-container{ + .main-content{ -.auth-container { - background: url(./images/auth/bg.png); + } +} +.main-bg-container { + background: url(./images/auth/bg.jpg); + .main-content{ + display: flex; + align-items:center; + justify-content: center; + background: rgba(255, 255, 255, 0.07); + backdrop-filter: blur(2px); + width: 100vw; + height: 100vh; + } } .semi-dropdown-item-active { @@ -63,6 +78,17 @@ } } } +.semi-radio-inner{ + width: 20px; + height: 20px; + .semi-radio-inner-display{ + width: 20px; + height: 20px; + .semi-icon{ + font-size: 18px; + } + } +} .dashboard-menu-container { padding: var(--dashboard-layout-padding,15px); diff --git a/src/components/money-format.tsx b/src/components/money-format.tsx index 7186b81..b7d72db 100644 --- a/src/components/money-format.tsx +++ b/src/components/money-format.tsx @@ -1,11 +1,29 @@ import React from "react"; type MoneyFormatProps = { - money:number|string; - currency?:'USD'|'HKD'|'RMB'; + money: number | string; + currency?: 'USD' | 'HKD' | 'RMB'; } -const MoneyFormat:React.FC = ({money,currency = 'HKD'})=>{ + +function formatCurrency(amount: string | number) { + // 将金额转换为字符串,并限制小数点后两位 + let amountStr = Number(amount).toFixed(2); + + // 移除可能的负号 + const isNegative = amountStr[0] === '-'; + if (isNegative) { + amountStr = amountStr.slice(1); + } + // 将金额字符串拆分为整数和小数部分 + const [intPart, decimalPart] = amountStr.split('.'); + // 整数部分每三位添加一个逗号 + const formattedIntPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ','); + // 返回格式化后的金额 + return (isNegative ? '-' : '') + formattedIntPart + '.' + decimalPart; +} + +const MoneyFormat: React.FC = ({money, currency = 'HKD'}) => { // 将货币数字转换为千分位格式且带2位小数 - return {Number(money).toLocaleString('zh-HK', {style: 'currency', currency})} + return {currency} {formatCurrency(money)} } export default MoneyFormat; \ No newline at end of file diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json index 989143e..1844617 100644 --- a/src/i18n/translations/en.json +++ b/src/i18n/translations/en.json @@ -1,5 +1,7 @@ { "bill": { + "bill_number": "Bill Number", + "download_receipt": "Download receipt", "title_actual_payment_amount": "Actually Paid", "title_amount": "Amount", "title_bill_detail": "Bill Detail", @@ -42,5 +44,11 @@ "exp_time": "bill will expires at", "student_number": "Student Number", "student_number_required": "required student number" + }, + "pay": { + "amount": "Amount", + "charge": "Charge", + "text_success": "Success", + "total_amount": "Total Amount HKD" } } \ No newline at end of file diff --git a/src/i18n/translations/sc.json b/src/i18n/translations/sc.json index 1a691f5..a7d4797 100644 --- a/src/i18n/translations/sc.json +++ b/src/i18n/translations/sc.json @@ -1,5 +1,7 @@ { "bill": { + "bill_number": "账单编号", + "download_receipt": "下载收据", "title_actual_payment_amount": "实付金额", "title_amount": "账单金额", "title_bill_detail": "账单详情", @@ -42,5 +44,11 @@ "exp_time": "账单过期时间", "student_number": "学号", "student_number_required": "请填写学号" + }, + "pay": { + "amount": "应付金额", + "charge": "手续费", + "text_success": "支付成功", + "total_amount": "应付总金额" } } \ No newline at end of file diff --git a/src/i18n/translations/tc.json b/src/i18n/translations/tc.json index 68f5475..e75ef2d 100644 --- a/src/i18n/translations/tc.json +++ b/src/i18n/translations/tc.json @@ -1,5 +1,7 @@ { "bill": { + "bill_number": "账单编号", + "download_receipt": "下载收据", "title_actual_payment_amount": "實付金額", "title_amount": "帳單金額", "title_bill_detail": "帳單詳情", @@ -42,5 +44,11 @@ "exp_time": "帳單過期時間", "student_number": "學號", "student_number_required": "請填入學號" + }, + "pay": { + "amount": "应付金额", + "charge": "手续费", + "text_success": "支付成功", + "total_amount": "应付总金额" } } \ No newline at end of file diff --git a/src/pages/auth/login.tsx b/src/pages/auth/login.tsx index 0a501b5..dc6a3b4 100644 --- a/src/pages/auth/login.tsx +++ b/src/pages/auth/login.tsx @@ -43,6 +43,9 @@ const AuthLogin = () => { + ) } diff --git a/src/pages/pay/component/index.tsx b/src/pages/pay/component/index.tsx new file mode 100644 index 0000000..0a1fbab --- /dev/null +++ b/src/pages/pay/component/index.tsx @@ -0,0 +1,10 @@ +import AppLogo from "@/assets/AppLogo.tsx"; +import useConfig from "@/hooks/useConfig.ts"; + +export const PayLogo = () => { + const {appName} = useConfig() + return (
+ + {appName &&

{appName}

} +
) +} \ No newline at end of file diff --git a/src/pages/pay/index.tsx b/src/pages/pay/index.tsx new file mode 100644 index 0000000..c5a4e34 --- /dev/null +++ b/src/pages/pay/index.tsx @@ -0,0 +1,83 @@ +import styles from './pay.module.less' +import {PayLogo} from "@/pages/pay/component"; +import {useTranslation} from "react-i18next"; +import MoneyFormat from "@/components/money-format.tsx"; +import {useState} from "react"; +import {Radio, RadioGroup} from '@douyinfe/semi-ui'; +import FlywireLogo from "@/assets/images/pay/flywire.tsx"; +import AsiaPayLogo from "@/assets/images/pay/asia_pay.tsx"; +import {useNavigate} from "react-router-dom"; + +const BillDetail = [ + {title: 'TUITION FEE', value: 67500}, + {title: 'CAUTION FEE', value: 400}, + {title: 'VISA FEE', value: 500}, +] +const PayIndex = () => { + const {t} = useTranslation() + const [payChannel, setPayChannel] = useState('asia_pay') + const navigate = useNavigate() + const startPay = ()=>{ + navigate('/success?id=123123') + } + return (
+ +
+
{t('pay.total_amount')}
+
+ +
+
+
+ {t('pay.amount')}: + +
+ {payChannel == 'asia_pay' &&
+ AsiaPay{t('pay.charge')}: + +
} +
+
+
+ setPayChannel(e.target.value)} value={payChannel}> + + + AsiaPay + + + + Flywire + + +
+
+
+
Payment Type:
+
OFFER FEE
+
+ {BillDetail.map((it, idx) => ( +
+
{it.title}:
+
+
+ ))} +
+
+
+ Bill Number: + 20240509114 +
+
+ Order Number: + PM-202405211114 +
+
+
+
Your Email: xxxx@google.com
+
+ +
+
+
); +} +export default PayIndex; \ No newline at end of file diff --git a/src/pages/pay/pay.module.less b/src/pages/pay/pay.module.less new file mode 100644 index 0000000..8bee6ed --- /dev/null +++ b/src/pages/pay/pay.module.less @@ -0,0 +1,122 @@ +.border-radius { + border-radius: var(--main-border-radius, 10px); +} + +.container { + .border-radius; + padding: 30px 80px; + background: #fff; + width: 600px; + transition: width 0.1s; +} + +.payAmount { + color: #fff; + background-color: #50ADA7; + .border-radius; + padding: 25px; + margin: 40px 0 10px; + font-size: 14px; +} + +.totalAmount { + font-size: 36px; + margin: 10px 0; +} + +.payChanel { + margin: 20px; + text-align: center; +} + +.itemValue { + + color: #999999; + text-align: left; + padding-left: 10px; +} + +.billInfo { + font-size: 12px; + line-height: 20px; + margin-top: 50px; + + :global { + .value { + .itemValue; + } + } +} + +.payDetail { + font-size: 12px; + line-height: 20px; + + :global { + .pay-item { + display: flex; + } + + .title { + flex: 1; + text-align: right; + } + + .value { + flex: 1; + .itemValue; + } + } +} + +.payConfirm { + padding-top: 20px; + margin-top: 20px; + border-top: solid 1px #eee; + text-align: center; +} + +.btnConfirm { + width: 300px; + height: 34px; + background-color: #43ABFF; + border-radius: 20px; + color: #fff; + outline: none; + border: none; + margin-top: 20px; + cursor: pointer; + + &:active, &:hover { + background-color: #3d9dea; + } +} + +.paySuccess { + text-align: center; + margin: 50px 0; + :global{ + .icon{ + display: block; + margin: auto; + } + } +} + +.successIcon { + color: #74C041; + font-size: 100px; +} + +@media screen and (max-width: 700px) { + .container { + width: 80%; + padding: 30px 30px; + } + + .payAmount { + padding: 25px 10px; + margin-top: 20px; + font-size: 13px; + } +} \ No newline at end of file diff --git a/src/pages/pay/success.tsx b/src/pages/pay/success.tsx new file mode 100644 index 0000000..00695aa --- /dev/null +++ b/src/pages/pay/success.tsx @@ -0,0 +1,33 @@ +import styles from "@/pages/pay/pay.module.less"; +import {PayLogo} from "@/pages/pay/component"; +import {SuccessIcon} from "@/assets/images/pay/success.tsx"; +import {useTranslation} from "react-i18next"; +import {useDownloadReceiptPDF} from "@/service/generate-pdf.ts"; +import {Button} from "@douyinfe/semi-ui"; + +const PayIndex = () => { + const {t} = useTranslation() + const {downloadPDF} = useDownloadReceiptPDF() + + return (
+ +
+
+ +
+

+ {t('pay.text_success')} +

+
+
+
{t('bill.bill_number')}: PM-202405211114
+
+ +
+
+
); +} +export default PayIndex; \ No newline at end of file diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 7608bad..c40f07e 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,5 +1,7 @@ import {createBrowserRouter, Navigate, RouteObject, RouterProvider,} from "react-router-dom"; import {lazy, Suspense, useMemo,} from "react"; +import {useTranslation} from "react-i18next"; + import { LocaleProvider } from '@douyinfe/semi-ui'; import zh_CN from '@douyinfe/semi-ui/lib/es/locale/source/zh_CN'; import zh_TW from '@douyinfe/semi-ui/lib/es/locale/source/zh_TW'; @@ -8,8 +10,11 @@ import en_US from '@douyinfe/semi-ui/lib/es/locale/source/en_US'; import ErrorBoundary, {Error404} from "./error.tsx"; import DashboardLayout from "@/routes/layout/dashboard-layout.tsx"; import AuthLogin from "@/pages/auth/login.tsx"; -import AuthLayout from "@/routes/layout/auth-layout.tsx"; -import {useTranslation} from "react-i18next"; +import PayIndex from "@/pages/pay/index.tsx"; +import PaySuccess from "@/pages/pay/success.tsx"; +import MainLayout from "@/routes/layout/main-layout.tsx"; + +import Loader from "@/components/loader.tsx"; const ManualIndex = lazy(() => import("@/pages/manual/index.tsx")); const BillQuery = lazy(() => import("@/pages/bill/query.tsx")); @@ -19,7 +24,7 @@ const BillReconciliation = lazy(() => import("@/pages/bill/reconciliation.tsx")) const routes: RouteObject[] = [ { path: '/', - element: , + element: , errorElement: , children: [ { @@ -30,6 +35,14 @@ const routes: RouteObject[] = [ path: 'login', element: }, + { + path: 'pay', + element: + }, + { + path: 'success', + element: + }, ] }, { @@ -72,7 +85,7 @@ const AppRouter = () => { },[i18n.language]) return ( - + }> ) diff --git a/src/routes/layout/auth-layout.tsx b/src/routes/layout/auth-layout.tsx deleted file mode 100644 index 66134f0..0000000 --- a/src/routes/layout/auth-layout.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; -import {Outlet} from "react-router-dom"; -import styled from "@emotion/styled"; -import {I18nSwitcher} from "@/i18n"; - -const AuthContainer = styled.div({ - minHeight: '100vh', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', -}) -const I18nContainer = styled.div({ - position: 'absolute', - top: 10, - right: 10 -}) -const AuthLayout: React.FC<{ children?: React.ReactNode }> = ({children}) => { - return ( - - - - {children ? children : } - ) -} -export default AuthLayout \ No newline at end of file diff --git a/src/routes/layout/main-layout.tsx b/src/routes/layout/main-layout.tsx new file mode 100644 index 0000000..f68bc10 --- /dev/null +++ b/src/routes/layout/main-layout.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import {Outlet} from "react-router-dom"; +import styled from "@emotion/styled"; +import {I18nSwitcher} from "@/i18n"; + +const I18nContainer = styled.div({ + position: 'absolute', + top: 10, + right: 10 +}) +const MainLayout: React.FC<{ children?: React.ReactNode }> = ({children}) => { + return (
+
+ + + + {children ? children : } +
+
) +} +export default MainLayout \ No newline at end of file diff --git a/src/service/generate-pdf.ts b/src/service/generate-pdf.ts index be5f7c5..3d722d0 100644 --- a/src/service/generate-pdf.ts +++ b/src/service/generate-pdf.ts @@ -1,5 +1,7 @@ import JsPDF from "jspdf"; import autoTable from "jspdf-autotable"; +import {useState} from "react"; + function drawItem(doc: JsPDF, item: { title: string; @@ -70,4 +72,23 @@ export function GeneratePdf(filename: string) { drawItem(doc, {title: 'Please retain this acknowledgement receipt for your record.'}, 185) doc.save(filename); +} + + +// use service work generate pdf and download +export function useDownloadReceiptPDF() { + const [loading, setLoading] = useState(false); + + const downloadPDF = (bill: Partial) => { + setLoading(true) + setTimeout(() => { + GeneratePdf(`Receipt-${bill.id}.pdf`) + setLoading(false) + }, 100); + } + + return { + loading, + downloadPDF + } } \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index fbe757f..d911259 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,6 @@ import {defineConfig} from 'vite' import react from '@vitejs/plugin-react' -import * as path from "path"; +import {resolve} from "path"; // https://vitejs.dev/config/ export default defineConfig(({mode}) => { @@ -12,7 +12,7 @@ export default defineConfig(({mode}) => { }, resolve: { alias: { - '@': path.resolve(__dirname, './src') + '@': resolve(__dirname, './src') } } }