From 295d6c75e9b4e04316015ecf3c33e90fbaee8adb Mon Sep 17 00:00:00 2001 From: callmeyan Date: Mon, 20 May 2024 16:09:43 +0800 Subject: [PATCH] :sparkles: add bill list component --- index.html | 2 +- package.json | 4 + src/assets/AppLogo.tsx | 11 +- src/assets/index.less | 9 +- src/components/bill/list.tsx | 187 +++++++++++++++++++++ src/components/count-number.tsx | 25 --- src/components/logo/index.tsx | 18 +- src/components/money-format.tsx | 11 ++ src/contexts/auth/index.tsx | 4 +- src/i18n/translations/en.json | 22 +++ src/i18n/translations/sc.json | 22 +++ src/i18n/translations/tc.json | 22 +++ src/pages/bill/query.tsx | 12 +- src/pages/bill/reconciliation.tsx | 5 +- src/pages/manual/index.tsx | 66 +++++++- src/pages/manual/manual.module.less | 43 +++++ src/routes/error.tsx | 23 ++- src/routes/layout/dashboard-layout.tsx | 91 +++++++--- src/routes/layout/dashboard-navigation.tsx | 2 +- src/types/bill.d.ts | 34 ++++ src/types/core.d.ts | 11 ++ yarn.lock | 17 +- 22 files changed, 553 insertions(+), 88 deletions(-) create mode 100644 src/components/bill/list.tsx delete mode 100644 src/components/count-number.tsx create mode 100644 src/components/money-format.tsx create mode 100644 src/pages/manual/manual.module.less create mode 100644 src/types/bill.d.ts create mode 100644 src/types/core.d.ts diff --git a/index.html b/index.html index 257f224..1aeaede 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - AI安全监控系统 + Payment System
diff --git a/package.json b/package.json index 08b9622..33fe3e9 100644 --- a/package.json +++ b/package.json @@ -20,16 +20,20 @@ "@mui/system": "^5.15.15", "ahooks": "^3.7.11", "clsx": "^2.1.1", + "dayjs": "^1.11.11", + "file-saver": "^2.0.5", "i18next": "^23.11.4", "jspdf": "^2.5.1", "jspdf-autotable": "^3.8.2", "less": "^4.2.0", + "qrcode.react": "^3.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^14.1.1", "react-router-dom": "^6.23.1" }, "devDependencies": { + "@types/file-saver": "^2.0.7", "@types/lodash": "^4.17.1", "@types/node": "^20.12.11", "@types/react": "^18.2.66", diff --git a/src/assets/AppLogo.tsx b/src/assets/AppLogo.tsx index dc99790..32acd7d 100644 --- a/src/assets/AppLogo.tsx +++ b/src/assets/AppLogo.tsx @@ -1,12 +1,13 @@ import React from "react"; - const AppLogo = ({style}: { style?: React.CSSProperties }) => { + const AppLogo = ({style,theme}: { style?: React.CSSProperties,theme?:'origin'|'color' }) => { + const currentColor = theme == 'color' && style?.color ? style.color : null return ( - - - - + + + + ) } diff --git a/src/assets/index.less b/src/assets/index.less index 335846f..a4e7220 100644 --- a/src/assets/index.less +++ b/src/assets/index.less @@ -8,7 +8,9 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; //user-select: none; - + --dashboard-header-height: 50px; + --dashboard-layout-padding:20px; + --dashboard-navigation-width:250px; } * { @@ -25,6 +27,9 @@ .semi-layout { min-height: 100vh; } +.text-center{ + text-align: center; +} .space-between { justify-content: space-between; @@ -50,7 +55,7 @@ } .dashboard-menu-container { - padding: 15px; + padding: var(--dashboard-layout-padding,15px); .nav-item { padding: 15px; display: flex; diff --git a/src/components/bill/list.tsx b/src/components/bill/list.tsx new file mode 100644 index 0000000..6d24f7b --- /dev/null +++ b/src/components/bill/list.tsx @@ -0,0 +1,187 @@ +import {Table, Typography} from "@douyinfe/semi-ui"; +import {ColumnProps} from "@douyinfe/semi-ui/lib/es/table"; +import dayjs from "dayjs"; +import React, {useMemo, useState} from "react"; +import {useTranslation} from "react-i18next"; +import MoneyFormat from "@/components/money-format.tsx"; +import {clone} from "lodash"; + +type BillListProps = { + type: 'query' | 'reconciliation'; + operationRender?: (record: BillModel) => React.ReactNode; + onRowSelection?: (selectedRowKeys?: (string | number)[]) => void; +} + +const mockBill: BillModel = { + actual_payment_amount: 110, + amount: 110, + application_no: "123123", + apply_status: "pending", + bill_status: "pending", + created_at: new Date(), + currency: "HKD", + department: "经管学院", + expiration_time: dayjs().add(30, "minute").toDate(), + id: 1123123, + paid_area: "HongKong,China", + paid_at: new Date(), + pay_amount: 110, + pay_method: "WechatHk", + payment_channel: "AsiaPay", + program_id: 123123, + service_charge: 0, + student_en_name: "SanF Chung", + student_no: "123123", + student_sc_name: "张三丰", + student_semester: "1", + student_tc_name: "张三丰", + student_year: "2024", + updated_at: "", + detail: [ + {amount: 55, bill_id: 1123123, id: 1, type: "APPLICATION FEE"}, + {amount: 50, bill_id: 1123123, id: 1, type: "TUITION FEE"}, + {amount: 5, bill_id: 1123123, id: 1, type: "VISA FEE"}, + ] +} + +export const BillList: React.FC = (props) => { + const {t, i18n} = useTranslation() + const [data, setData] = useState>({ + list: Array(10).fill(mockBill).map((it, i) => { + const s = clone(it); + s.id = i; + if (i % 2 == 0) s.service_charge = 10 + return s; + }), + pagination: {current: 1, pageSize: 10, total: 250} + }); + console.log(data) + const [loading, setLoading] = useState(false); + + const handlePageChange = (current: number) => { + setLoading(true) + setTimeout(() => { + setData({ + ...data, + pagination: {current, pageSize: 10, total: 250} + }) + setLoading(false) + }, 500) + }; + + const scroll = useMemo(() => ({y: 600, x: 800}), []); + + const columns = useMemo[]>(() => { + const cols: ColumnProps[] = [ + { + title: '#', + dataIndex: 'id', + width: 80, + fixed: true, + }, + { + title: t('bill.title_student_name_en'), + dataIndex: 'student_en_name', + fixed: true, + }, + { + title: t('bill.title_student_name_sc'), + dataIndex: 'student_sc_name', + render: (_, record) => (`${record.student_tc_name}(${record.student_sc_name})`) + }, + { + title: t('bill.title_bill_detail'), + dataIndex: 'detail', + width: 200, + render: (_, record) => (
+ {record.detail.map(it => (
{it.type}:{it.amount}
))} +
), + }, + { + title: t('bill.title_amount'), + dataIndex: 'amount', + render: (_) => (), + }, + { + title: t('bill.title_pay_amount'), + dataIndex: 'pay_amount', + render: (_, record) => { + if (record.service_charge > 0) { + return
+
+ + {t('bill.title_service_charge')}: + +
+ } + return (
HK$ {_}
) + }, + }, + { + title: t('bill.title_actual_payment_amount'), + dataIndex: 'actual_payment_amount', + render: (_) => (), + }, + { + title: t('bill.title_pay_method'), + dataIndex: 'pay_method', + render: (_, {pay_method, payment_channel}) => (
+ {payment_channel}
+ + ({pay_method}) + +
), + }, + { + title: t('bill.title_bill_status'), + dataIndex: 'bill_status', + // render: value => dayjs(value).format('YYYY-MM-DD'), + }, + ] + if (props.type != 'reconciliation') { + cols.push({ + title: t('bill.title_reconciliation_status'), + dataIndex: 'apply_status', + // render: value => dayjs(value).format('YYYY-MM-DD'), + }) + } + if (props.operationRender) { + cols.push({ + title: '', + dataIndex: 'operate', + fixed: 'right', + width: 300, + render: (_, record) => props.operationRender?.(record), + }) + } + return cols; + }, [props.operationRender, props.type, i18n.language]); + + + return
+ +
+ + sticky={{top: 60}} + bordered + scroll={scroll} + columns={columns} + dataSource={data.list} + rowKey={'id'} + pagination={{ + currentPage: data.pagination.current, + pageSize: data.pagination.pageSize, + total: data.pagination.total, + onPageChange: handlePageChange, + }} + loading={loading} + rowSelection={props.onRowSelection? { + fixed: true, + onChange: (selectedRowKeys, _selectedRows) => { + props.onRowSelection?.(selectedRowKeys) + } + }:undefined} + /> +
+
+} \ No newline at end of file diff --git a/src/components/count-number.tsx b/src/components/count-number.tsx deleted file mode 100644 index c86da43..0000000 --- a/src/components/count-number.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import {useCountUp} from "react-countup"; -import React, {useEffect, useRef} from "react"; - -type CountNumberProps = { - duration?: number; - end: number; -} -export const CountNumber: React.FC = (props) => { - const countUpRef = useRef(null); - const {update} = useCountUp({ - ref: countUpRef, - start: 0, - end: props.end, - duration: props.duration, - }); - - useEffect(() => { - if(props.end > 0){ - update(props.end); - } - }, [props]) - - // return - return -} \ No newline at end of file diff --git a/src/components/logo/index.tsx b/src/components/logo/index.tsx index 076d017..ab12516 100644 --- a/src/components/logo/index.tsx +++ b/src/components/logo/index.tsx @@ -8,7 +8,7 @@ export const IconQRCode = ({style}: { style?: React.CSSProperties }) => ( - + ( - + + fill="currentColor"/> + fill="currentColor"/> + fill="currentColor"/> @@ -87,21 +87,21 @@ export const IconReconciliation = ({style}: { style?: React.CSSProperties }) => - + + fill="currentColor"/> + fill="currentColor"/> + fill="currentColor"/> diff --git a/src/components/money-format.tsx b/src/components/money-format.tsx new file mode 100644 index 0000000..7186b81 --- /dev/null +++ b/src/components/money-format.tsx @@ -0,0 +1,11 @@ +import React from "react"; + +type MoneyFormatProps = { + money:number|string; + currency?:'USD'|'HKD'|'RMB'; +} +const MoneyFormat:React.FC = ({money,currency = 'HKD'})=>{ + // 将货币数字转换为千分位格式且带2位小数 + return {Number(money).toLocaleString('zh-HK', {style: 'currency', currency})} +} +export default MoneyFormat; \ No newline at end of file diff --git a/src/contexts/auth/index.tsx b/src/contexts/auth/index.tsx index 4b3a7e4..676c8fa 100644 --- a/src/contexts/auth/index.tsx +++ b/src/contexts/auth/index.tsx @@ -50,7 +50,7 @@ export const AuthProvider = ({children}: { children: React.ReactNode }) => { isLoggedIn: true, user: { id: 1, - nickname: 'test-user', + nickname: 'TU', department: 'root', } } @@ -66,7 +66,7 @@ export const AuthProvider = ({children}: { children: React.ReactNode }) => { isLoggedIn: true, user: { id: 1, - nickname: 'test-user', + nickname: 'TU', department: 'root', } } diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json index ebe9c23..9140481 100644 --- a/src/i18n/translations/en.json +++ b/src/i18n/translations/en.json @@ -1,4 +1,16 @@ { + "bill": { + "title_actual_payment_amount": "Actually Paid", + "title_amount": "Amount", + "title_bill_detail": "Bill Detail", + "title_bill_status": "Bill Status", + "title_pay_amount": "Pay Amount", + "title_pay_method": "Pay Method", + "title_reconciliation_status": "Reconciliation", + "title_service_charge": "Service Charge", + "title_student_name_en": "English Name", + "title_student_name_sc": "Chinese Name" + }, "error": { "go_back": "Go Back", "go_home": "Go Home" @@ -14,5 +26,15 @@ "login": { "submit": "LOGIN WITH SSO", "title": "Login" + }, + "manual": { + "amount": "Amount", + "amount_required": "require bill amount", + "bill_type": "Bill Type", + "bill_type_required": "please select bill type", + "btn_generate": "Generate Bill", + "exp_time": "bill will expires at", + "student_number": "Student Number", + "student_number_required": "required student number" } } \ No newline at end of file diff --git a/src/i18n/translations/sc.json b/src/i18n/translations/sc.json index c8fea40..be1a366 100644 --- a/src/i18n/translations/sc.json +++ b/src/i18n/translations/sc.json @@ -1,4 +1,16 @@ { + "bill": { + "title_actual_payment_amount": "实付金额", + "title_amount": "账单金额", + "title_bill_detail": "账单详情", + "title_bill_status": "账单状态", + "title_pay_amount": "应付金额", + "title_pay_method": "支付方式", + "title_reconciliation_status": "对账状态", + "title_service_charge": "手续费", + "title_student_name_en": "英文名称", + "title_student_name_sc": "中文名字" + }, "error": { "go_back": "返回上一页", "go_home": "回到首页" @@ -14,5 +26,15 @@ "login": { "submit": "使用SSO登录", "title": "登录" + }, + "manual": { + "amount": "金额", + "amount_required": "请填写账单金额", + "bill_type": "账单类型", + "bill_type_required": "请选择账单类型", + "btn_generate": "生成账单", + "exp_time": "账单过期时间", + "student_number": "学号", + "student_number_required": "请填写学号" } } \ No newline at end of file diff --git a/src/i18n/translations/tc.json b/src/i18n/translations/tc.json index 1f8ab05..c004521 100644 --- a/src/i18n/translations/tc.json +++ b/src/i18n/translations/tc.json @@ -1,4 +1,16 @@ { + "bill": { + "title_actual_payment_amount": "实付金额", + "title_amount": "账单金额", + "title_bill_detail": "账单详情", + "title_bill_status": "账单状态", + "title_pay_amount": "应付金额", + "title_pay_method": "支付方式", + "title_reconciliation_status": "对账状态", + "title_service_charge": "手续费", + "title_student_name_en": "英文名称", + "title_student_name_sc": "中文名字" + }, "error": { "go_back": "返回上一页", "go_home": "回到首页" @@ -14,5 +26,15 @@ "login": { "submit": "使用SSO登入", "title": "登入" + }, + "manual": { + "amount": "金额", + "amount_required": "请填写账单金额", + "bill_type": "账单类型", + "bill_type_required": "请选择账单类型", + "btn_generate": "生成账单", + "exp_time": "账单过期时间", + "student_number": "学号", + "student_number_required": "请填写学号" } } \ No newline at end of file diff --git a/src/pages/bill/query.tsx b/src/pages/bill/query.tsx index 28d8eca..ea27d43 100644 --- a/src/pages/bill/query.tsx +++ b/src/pages/bill/query.tsx @@ -1,7 +1,17 @@ +import {BillList} from "@/components/bill/list.tsx"; +import {Button, Space} from "@douyinfe/semi-ui"; +import {GeneratePdf} from "@/service/generate-pdf.ts"; const BillQuery = () => { + const operation = (_record:BillModel)=>{ + return ( + + + + ) + } return (
-

BillQuery

+
) } export default BillQuery \ No newline at end of file diff --git a/src/pages/bill/reconciliation.tsx b/src/pages/bill/reconciliation.tsx index e88de97..0b21246 100644 --- a/src/pages/bill/reconciliation.tsx +++ b/src/pages/bill/reconciliation.tsx @@ -1,7 +1,10 @@ +import {BillList} from "@/components/bill/list.tsx"; const BillReconciliation = () => { return (
-

BillQuery

+ { + console.log('xxx',keys) + }} />
) } export default BillReconciliation \ No newline at end of file diff --git a/src/pages/manual/index.tsx b/src/pages/manual/index.tsx index f3a3c52..d14393d 100644 --- a/src/pages/manual/index.tsx +++ b/src/pages/manual/index.tsx @@ -1,10 +1,64 @@ -import {Button} from "@douyinfe/semi-ui"; -import {GeneratePdf} from "@/service/generate-pdf.ts"; - +import {Button, Form, Space} from "@douyinfe/semi-ui"; +import {useTranslation} from "react-i18next"; +import {saveAs } from "file-saver" +import QRCode from "qrcode.react"; +import {useRef} from "react"; +import styles from './manual.module.less' +const BillDetailItem = (item:{title:string;value:string})=>{ + return
+
{item.title} :
+
{item.value}
+
+} export default function Index() { - return (
-

Index

- + const {t} = useTranslation() + const qrCodeRef = useRef(null) + const downloadQRCode = ()=>{ + const canvas = qrCodeRef.current?.querySelector('canvas'); + if(!canvas) return + saveAs(canvas.toDataURL(), 'qrcode.png') + } + return (
+
+
console.log(values)}> + + TUITION FEE + CAUTION FEE + APPLICATION FEE + + + +
+ +
+ +
+ +
+ + + + +
+
+
+
+ +
+
+
{t('manual.exp_time')} {'12:00'}
+
+
) } \ No newline at end of file diff --git a/src/pages/manual/manual.module.less b/src/pages/manual/manual.module.less new file mode 100644 index 0000000..6199f8e --- /dev/null +++ b/src/pages/manual/manual.module.less @@ -0,0 +1,43 @@ +.manualPage { + +} + +.generateForm { + min-height: 90px; +} + +.generateButtonContainer { + padding-top: 24px; +} + +.billDetail { + + padding: 30px 0 50px; +} + +.billDetailItem { + display: flex; + margin-bottom: 10px; + align-items: center; +} +.billDetailItemTitle { + font-weight: bold; + width: 150px; +} + +.billDetailItemValue { + color: #999999; +} +.billQrCode{ + text-align: center; + margin-left: 150px; +} +.QRCodeContainer{ + margin-bottom: 20px; +} +.qrCode{ + display: inline-block; + background-color: #f8f9fa; + padding: 25px; + border: dashed 1px #ccc; +} \ No newline at end of file diff --git a/src/routes/error.tsx b/src/routes/error.tsx index d1b4493..e3397b5 100644 --- a/src/routes/error.tsx +++ b/src/routes/error.tsx @@ -1,14 +1,14 @@ import React from "react"; -import {isRouteErrorResponse, Link, useNavigate, useRouteError} from 'react-router-dom'; +import {isRouteErrorResponse, useNavigate, useRouteError} from 'react-router-dom'; import {Button, Col, Row, Space, Typography} from "@douyinfe/semi-ui"; +import {Box, Stack} from "@mui/system"; +import {useTranslation} from "react-i18next"; +import styled from "@emotion/styled"; // material-ui import error500 from "@/assets/images/error/Error500.png"; import error404 from "@/assets/images/error/Error404.png"; import TwoCone from "@/assets/images/error/TwoCone.png"; -import {Box, Stack} from "@mui/system"; -import {useTranslation} from "react-i18next"; -import styled from "@emotion/styled"; // ==============================|| ELEMENT ERROR - COMMON ||============================== // const ErrorContainer = styled.div({ @@ -57,8 +57,19 @@ const ErrorBoundary: React.FC<{ {errorMessage} {process.env.NODE_ENV == 'development' &&
-
-                            {error.stack}
+                        
+                            {error.stack}
                         
} diff --git a/src/routes/layout/dashboard-layout.tsx b/src/routes/layout/dashboard-layout.tsx index d29188b..d252cce 100644 --- a/src/routes/layout/dashboard-layout.tsx +++ b/src/routes/layout/dashboard-layout.tsx @@ -1,14 +1,16 @@ -import {Outlet} from "react-router-dom"; -import React from "react"; -import {Avatar, Dropdown, Layout, Nav, Space} from "@douyinfe/semi-ui" +import {Outlet, useLocation} from "react-router-dom"; +import React, {useMemo} from "react"; +import {Avatar, Dropdown, Layout, Nav, Space, Typography} from "@douyinfe/semi-ui" import {useTranslation} from "react-i18next"; import AuthGuard from "@/routes/layout/auth-guard.tsx"; import AppLogo from "@/assets/AppLogo.tsx"; import useAuth from "@/hooks/useAuth.ts"; import {I18nSwitcher} from "@/i18n"; -import {DashboardNavigation} from "@/routes/layout/dashboard-navigation.tsx"; -import { IconExit } from "@douyinfe/semi-icons"; +import {AllDashboardMenu, DashboardNavigation} from "@/routes/layout/dashboard-navigation.tsx"; +import {IconExit} from "@douyinfe/semi-icons"; +import styled from "@emotion/styled"; +import useConfig from "@/hooks/useConfig.ts"; const {Header, Content, Sider} = Layout; @@ -17,26 +19,52 @@ type CommonHeaderProps = { title?: React.ReactNode; rightExtra?: React.ReactNode; } +const HeaderUserProfile = styled.div({ + padding: '10px 20px 0', + minWidth: 120, + fontSize: 12, + lineHeight: 1 +}) export const HeaderUserAvatar = () => { const {t} = useTranslation() const {user, logout} = useAuth() return ( - } onClick={logout}>{t('layout.logout')} - + <> + + + {user?.nickname} +
+ {user?.nickname} + DEPT:{user?.department?.toUpperCase()} +
+
+
+ + + } onClick={logout}>{t('layout.logout')} + + } > + {user?.nickname}
) } export const CommonHeader: React.FC = ({children, title, rightExtra}) => { - return (
+ const {appName} = useConfig() + return (
-