diff --git a/src/App.tsx b/src/App.tsx index ae82291..21e7d89 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,13 +2,12 @@ import AppRouter from "@/routes"; import {ConfigProvider} from "@/contexts/config"; import {AuthProvider} from "@/contexts/auth"; import '@/i18n/config.ts'; + function App() { return ( -
- -
+
) diff --git a/src/assets/index.less b/src/assets/index.less index a4e7220..5a93a4c 100644 --- a/src/assets/index.less +++ b/src/assets/index.less @@ -30,6 +30,9 @@ .text-center{ text-align: center; } +.text-right{ + text-align: right; +} .space-between { justify-content: space-between; @@ -53,6 +56,13 @@ .semi-dropdown-item-active { background-color: var(--semi-color-default-active); } +.semi-tabs{ + &.no-border{ + .semi-tabs-bar-line.semi-tabs-bar-top{ + border-bottom: none; + } + } +} .dashboard-menu-container { padding: var(--dashboard-layout-padding,15px); diff --git a/src/components/bill/bill.less b/src/components/bill/bill.less new file mode 100644 index 0000000..6704504 --- /dev/null +++ b/src/components/bill/bill.less @@ -0,0 +1,30 @@ +.bill-search-form{ + +} + +.bill-info { + font-size: 14px; + text-align: right; + line-height: 1.5; + + .bill-info-item { + display: flex; + align-items: center; + justify-content: end; + + .bill-info-title { + display: flex; + align-items: center; + + &:before { + content: ' '; + width: 6px; + height: 6px; + display: block; + background-color: #00C479; + margin-right: 5px; + border-radius: 50%; + } + } + } +} \ No newline at end of file diff --git a/src/components/bill/detail.tsx b/src/components/bill/detail.tsx new file mode 100644 index 0000000..524fefc --- /dev/null +++ b/src/components/bill/detail.tsx @@ -0,0 +1,43 @@ +import styles from "@/pages/manual/manual.module.less"; +import {Button, Space} from "@douyinfe/semi-ui"; +import QRCode from "qrcode.react"; +import {useTranslation} from "react-i18next"; +import {useRef} from "react"; +import {saveAs} from "file-saver"; + + +const BillDetailItem = (item:{title:string;value:string})=>{ + return
+
{item.title} :
+
{item.value}
+
+} + +const BillDetail:BasicComponent<{bill:BillModel}> = ()=>{ + const {t} = useTranslation(); + const qrCodeRef = useRef(null) + const downloadQRCode = ()=>{ + const canvas = qrCodeRef.current?.querySelector('canvas'); + if(!canvas) return + saveAs(canvas.toDataURL(), 'qrcode.png') + } + return
+ +
+
+
+ +
+
+
{t('manual.exp_time')} {'12:00'}
+
+
+ + + + +
+
+
+} +export default BillDetail \ No newline at end of file diff --git a/src/components/bill/list.tsx b/src/components/bill/list.tsx index 6d24f7b..4ae227f 100644 --- a/src/components/bill/list.tsx +++ b/src/components/bill/list.tsx @@ -1,75 +1,23 @@ 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 React, {useMemo} from "react"; import {useTranslation} from "react-i18next"; import MoneyFormat from "@/components/money-format.tsx"; -import {clone} from "lodash"; +import {Card} from "@/components/card"; +import './bill.less' +import dayjs from "dayjs"; 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"}, - ] + source: RecordList; + onPageChange: () => void; + loading?: boolean; } 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[] = [ @@ -79,10 +27,30 @@ export const BillList: React.FC = (props) => { width: 80, fixed: true, }, + { + title: t('bill.title_paid_at'), + dataIndex: 'paid_at', + render: (value) => dayjs(value).format('YYYY-MM-DD'), + }, + { + title: t('bill.title_program_id'), + dataIndex: 'program_id', + }, + { + title: t('bill.title_department'), + dataIndex: 'department', + }, + { + title: t('bill.title_year'), + dataIndex: 'student_year', + }, + { + title: t('bill.title_semester'), + dataIndex: 'student_semester', + }, { title: t('bill.title_student_name_en'), dataIndex: 'student_en_name', - fixed: true, }, { title: t('bill.title_student_name_sc'), @@ -92,9 +60,10 @@ export const BillList: React.FC = (props) => { { title: t('bill.title_bill_detail'), dataIndex: 'detail', - width: 200, - render: (_, record) => (
- {record.detail.map(it => (
{it.type}:{it.amount}
))} + ellipsis: {showTitle: true}, + width: 220, + render: (_, record) => (
+ {record.detail.map((it,idx) => (
{it.type}:
))}
), }, { @@ -157,31 +126,46 @@ export const BillList: React.FC = (props) => { return cols; }, [props.operationRender, props.type, i18n.language]); + const currentTotalAmount = useMemo(()=>{ + // 计算当前列表总金额 + return props.source.list.map(s=>s.amount).reduce((s,c)=>(s+c),0) + },[props.source]) - return
- + return +
+ 查询总金额: + +
+
+ 当前页总金额: + +
+
} + >
sticky={{top: 60}} bordered - scroll={scroll} + scroll={{x: 600,y:300,scrollToFirstRowOnChange:true}} columns={columns} - dataSource={data.list} + dataSource={props.source.list} rowKey={'id'} pagination={{ - currentPage: data.pagination.current, - pageSize: data.pagination.pageSize, - total: data.pagination.total, - onPageChange: handlePageChange, + currentPage: props.source.pagination.current, + pageSize: props.source.pagination.pageSize, + total: props.source.pagination.total, + onPageChange: props.onPageChange, }} - loading={loading} - rowSelection={props.onRowSelection? { + loading={props.loading} + rowSelection={props.onRowSelection ? { fixed: true, - onChange: (selectedRowKeys, _selectedRows) => { + onChange: (selectedRowKeys) => { props.onRowSelection?.(selectedRowKeys) } - }:undefined} + } : undefined} />
-
+ } \ No newline at end of file diff --git a/src/components/bill/search-form.tsx b/src/components/bill/search-form.tsx new file mode 100644 index 0000000..d9b5a93 --- /dev/null +++ b/src/components/bill/search-form.tsx @@ -0,0 +1,61 @@ +import {Button, Col, Form, Row} from "@douyinfe/semi-ui"; +import React from "react"; +import {Card} from "@/components/card"; + +type SearchFormProps = { + onSearch?: () => void; + showApply?: boolean; + searchHeader?: React.ReactNode; + searchFooter?: React.ReactNode; +} + +const SearchForm: React.FC = (props) => { + + return ( + {props.searchHeader} +
+
+ + + + + + + + + + + + + + AsiaPay + FlyWire + PPS + + + + + 未支付 + 已支付 + 已作废 + + + {props.showApply && + + 未对账 + 已对账 + + } + + + + +
+
+ {props.searchFooter} +
) +} + +export default SearchForm; \ No newline at end of file diff --git a/src/components/card/card.less b/src/components/card/card.less index eb77c84..d2f6625 100644 --- a/src/components/card/card.less +++ b/src/components/card/card.less @@ -1,14 +1,18 @@ .card-container { - --card-padding: 10px 15px; background-color: #ffffff; border-radius: 5px; + padding: 20px; + &.card-padding{ + } .card-header { - padding: 10px 15px 30px 15px; + margin-bottom: 10px; color: #666; + display: flex; + justify-content: space-between; + align-items: flex-start; + font-size: 20px; } .card-body{ - padding: 0px 15px 10px; - min-height: 100px; } } \ No newline at end of file diff --git a/src/components/card/index.tsx b/src/components/card/index.tsx index f96e412..4f924f5 100644 --- a/src/components/card/index.tsx +++ b/src/components/card/index.tsx @@ -1,16 +1,21 @@ -import React from "react"; +import React, {CSSProperties} from "react"; import './card.less' +import {clsx} from "clsx"; type CardProps = { title?: React.ReactNode; + headerRight?: React.ReactNode; children?: React.ReactNode; bordered?: boolean; + style?: CSSProperties; } export const Card: React.FC = (props) => { - return (
- {props.title &&
+ + return (
+ {(props.title || props.headerRight) &&
{props.title}
+
{props.headerRight}
}
{props.children}
) diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json index 9140481..989143e 100644 --- a/src/i18n/translations/en.json +++ b/src/i18n/translations/en.json @@ -3,13 +3,19 @@ "title_actual_payment_amount": "Actually Paid", "title_amount": "Amount", "title_bill_detail": "Bill Detail", + "title_bill_list": "Bill List", "title_bill_status": "Bill Status", + "title_department": "Department", + "title_paid_at": "Transaction Date", "title_pay_amount": "Pay Amount", "title_pay_method": "Pay Method", + "title_program_id": "Program ID", "title_reconciliation_status": "Reconciliation", + "title_semester": "Semester", "title_service_charge": "Service Charge", "title_student_name_en": "English Name", - "title_student_name_sc": "Chinese Name" + "title_student_name_sc": "Chinese Name", + "title_year": "Year" }, "error": { "go_back": "Go Back", diff --git a/src/i18n/translations/sc.json b/src/i18n/translations/sc.json index be1a366..1a691f5 100644 --- a/src/i18n/translations/sc.json +++ b/src/i18n/translations/sc.json @@ -3,13 +3,19 @@ "title_actual_payment_amount": "实付金额", "title_amount": "账单金额", "title_bill_detail": "账单详情", + "title_bill_list": "账单列表", "title_bill_status": "账单状态", + "title_department": "学系", + "title_paid_at": "支付时间", "title_pay_amount": "应付金额", "title_pay_method": "支付方式", + "title_program_id": "专业ID", "title_reconciliation_status": "对账状态", + "title_semester": "学期", "title_service_charge": "手续费", "title_student_name_en": "英文名称", - "title_student_name_sc": "中文名字" + "title_student_name_sc": "中文名字", + "title_year": "学年" }, "error": { "go_back": "返回上一页", diff --git a/src/i18n/translations/tc.json b/src/i18n/translations/tc.json index c004521..68f5475 100644 --- a/src/i18n/translations/tc.json +++ b/src/i18n/translations/tc.json @@ -1,40 +1,46 @@ { "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": "中文名字" + "title_actual_payment_amount": "實付金額", + "title_amount": "帳單金額", + "title_bill_detail": "帳單詳情", + "title_bill_list": "帳單清單", + "title_bill_status": "帳單狀態", + "title_department": "學系", + "title_paid_at": "付款時間", + "title_pay_amount": "應付金額", + "title_pay_method": "付款方式", + "title_program_id": "專業ID", + "title_reconciliation_status": "對帳狀態", + "title_semester": "學期", + "title_service_charge": "手續費", + "title_student_name_en": "英文名稱", + "title_student_name_sc": "中文名字", + "title_year": "學年" }, "error": { - "go_back": "返回上一页", - "go_home": "回到首页" + "go_back": "返回上一頁", + "go_home": "回到首頁" }, "layout": { - "logout": "注销登录", + "logout": "登出登入", "menu": { - "bill": "账单查询", - "check": "对账", - "manual": "现场支付" + "bill": "帳單查詢", + "check": "對帳", + "manual": "現場支付" } }, "login": { "submit": "使用SSO登入", - "title": "登入" + "title": "登錄" }, "manual": { - "amount": "金额", - "amount_required": "请填写账单金额", - "bill_type": "账单类型", - "bill_type_required": "请选择账单类型", - "btn_generate": "生成账单", - "exp_time": "账单过期时间", - "student_number": "学号", - "student_number_required": "请填写学号" + "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 ea27d43..6626638 100644 --- a/src/pages/bill/query.tsx +++ b/src/pages/bill/query.tsx @@ -1,17 +1,77 @@ import {BillList} from "@/components/bill/list.tsx"; -import {Button, Space} from "@douyinfe/semi-ui"; +import {Button, Modal, Space} from "@douyinfe/semi-ui"; import {GeneratePdf} from "@/service/generate-pdf.ts"; +import {useState} from "react"; +import {clone} from "lodash"; +import dayjs from "dayjs"; +import SearchForm from "@/components/bill/search-form.tsx"; +import BillDetail from "@/components/bill/detail.tsx"; + +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"}, + ] +} const BillQuery = () => { + const [data, ] = 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,recordTotal:213123} + }); + const [showBill,setShowBill] = useState() const operation = (_record:BillModel)=>{ return ( - + ) } return (
- + + + {}} + onCancel={()=>setShowBill(undefined)} //>=1.16.0 + closeOnEsc={true} + footerFill={true} + > + {showBill && } +
) } export default BillQuery \ No newline at end of file diff --git a/src/pages/bill/reconciliation.tsx b/src/pages/bill/reconciliation.tsx index 0b21246..068671f 100644 --- a/src/pages/bill/reconciliation.tsx +++ b/src/pages/bill/reconciliation.tsx @@ -1,9 +1,22 @@ import {BillList} from "@/components/bill/list.tsx"; +import {useState} from "react"; +import SearchForm from "@/components/bill/search-form.tsx"; +import {TabPane, Tabs } from "@douyinfe/semi-ui"; const BillReconciliation = () => { + const [data, ] = useState>({ + list: [], pagination: {current: 0, pageSize: 0, total: 0} + }) return (
- { - console.log('xxx',keys) + + + 未对账} itemKey="1" /> + 已对账} itemKey="2" /> + } /> + { + console.log('xxx', keys); + }} onPageChange={function (): void { + throw new Error("Function not implemented."); }} />
) } diff --git a/src/pages/manual/index.tsx b/src/pages/manual/index.tsx index d14393d..c92e79e 100644 --- a/src/pages/manual/index.tsx +++ b/src/pages/manual/index.tsx @@ -4,6 +4,7 @@ import {saveAs } from "file-saver" import QRCode from "qrcode.react"; import {useRef} from "react"; import styles from './manual.module.less' +import {Card} from "@/components/card"; const BillDetailItem = (item:{title:string;value:string})=>{ return
@@ -19,46 +20,48 @@ export default function Index() { if(!canvas) return saveAs(canvas.toDataURL(), 'qrcode.png') } - return (
-
-
console.log(values)}> - - TUITION FEE - CAUTION FEE - APPLICATION FEE - - - -
- -
- -
- -
- - - - -
-
-
-
- + return ( +
+
+
console.log(values)}> + + TUITION FEE + CAUTION FEE + APPLICATION FEE + + + +
+
-
-
{t('manual.exp_time')} {'12:00'}
+
- -
) + +
+ + + + +
+
+
+
+ +
+
+
{t('manual.exp_time')} {'12:00'}
+
+
+
+ ) } \ No newline at end of file diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 5bc6475..7608bad 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,9 +1,15 @@ import {createBrowserRouter, Navigate, RouteObject, RouterProvider,} from "react-router-dom"; -import {lazy, Suspense,} from "react"; +import {lazy, Suspense, useMemo,} from "react"; +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'; +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"; const ManualIndex = lazy(() => import("@/pages/manual/index.tsx")); const BillQuery = lazy(() => import("@/pages/bill/query.tsx")); @@ -57,10 +63,19 @@ const router = createBrowserRouter([ ], {basename: import.meta.env.VITE_APP_BASE_NAME}) const AppRouter = () => { - return ( - - ) - // return () + // change ui locale + const {i18n} = useTranslation() + const locale = useMemo(()=>{ + if(i18n.language === 'zh-CN') return zh_CN; + else if(i18n.language === 'zh-TW') return zh_TW; + return en_US; + },[i18n.language]) + + return ( + + + + ) } export default AppRouter; \ No newline at end of file diff --git a/src/routes/layout/dashboard-layout.tsx b/src/routes/layout/dashboard-layout.tsx index d252cce..c37e6b0 100644 --- a/src/routes/layout/dashboard-layout.tsx +++ b/src/routes/layout/dashboard-layout.tsx @@ -96,9 +96,7 @@ type LayoutProps = { } const LayoutContentContainer = styled.div({ - backgroundColor: '#fff', borderRadius: 10, - padding: 'var(--dashboard-layout-padding)', marginTop:20 }) export const BaseLayout: React.FC = ({children}) => {