add bill list component

This commit is contained in:
LittleBoy 2024-05-20 16:09:43 +08:00
parent 44095afbc7
commit 295d6c75e9
22 changed files with 553 additions and 88 deletions

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI安全监控系统</title>
<title>Payment System</title>
</head>
<body>
<div id="root"></div>

View File

@ -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",

File diff suppressed because one or more lines are too long

View File

@ -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;

View File

@ -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<BillListProps> = (props) => {
const {t, i18n} = useTranslation()
const [data, setData] = useState<RecordList<BillModel>>({
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<ColumnProps<BillModel>[]>(() => {
const cols: ColumnProps<BillModel>[] = [
{
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) => (<div>
{record.detail.map(it => (<div>{it.type}:{it.amount}</div>))}
</div>),
},
{
title: t('bill.title_amount'),
dataIndex: 'amount',
render: (_) => (<MoneyFormat money={_}/>),
},
{
title: t('bill.title_pay_amount'),
dataIndex: 'pay_amount',
render: (_, record) => {
if (record.service_charge > 0) {
return <div>
<MoneyFormat money={record.pay_amount}/><br/>
<Typography.Text type={'quaternary'} size={'small'} style={{position: "absolute"}}>
{t('bill.title_service_charge')}: <MoneyFormat money={record.service_charge}/>
</Typography.Text>
</div>
}
return (<div>HK$ {_}</div>)
},
},
{
title: t('bill.title_actual_payment_amount'),
dataIndex: 'actual_payment_amount',
render: (_) => (<MoneyFormat money={_}/>),
},
{
title: t('bill.title_pay_method'),
dataIndex: 'pay_method',
render: (_, {pay_method, payment_channel}) => (<div>
{payment_channel}<br/>
<Typography.Text type={'quaternary'} size={'small'} style={{position: "absolute"}}>
({pay_method})
</Typography.Text>
</div>),
},
{
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 <div>
<div className="bill-list-table">
<Table<BillModel>
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}
/>
</div>
</div>
}

View File

@ -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<CountNumberProps> = (props) => {
const countUpRef = useRef<HTMLSpanElement>(null);
const {update} = useCountUp({
ref: countUpRef,
start: 0,
end: props.end,
duration: props.duration,
});
useEffect(() => {
if(props.end > 0){
update(props.end);
}
}, [props])
// return <CountUp end={props.end} duration={props.duration} />
return <span ref={countUpRef}></span>
}

View File

@ -8,7 +8,7 @@ export const IconQRCode = ({style}: { style?: React.CSSProperties }) => (
<rect x="0" y="0" width="15" height="15" rx="6"/>
</clipPath>
</defs>
<g clip-path="url(#master_svg0_1_3654/1_0549)">
<g clipPath="url(#master_svg0_1_3654/1_0549)">
<g>
<path
d="M1.40625,1.875C1.40625,1.616118,1.616118,1.40625,1.875,1.40625L5.625,1.40625C5.88388,1.40625,6.09375,1.616118,6.09375,1.875L6.09375,5.625C6.09375,5.88388,5.88388,6.09375,5.625,6.09375L1.875,6.09375C1.616118,6.09375,1.40625,5.88388,1.40625,5.625L1.40625,1.875ZM2.8125,4.6875L4.6875,4.6875L4.6875,2.8125L2.8125,2.8125L2.8125,4.6875Z"
@ -59,21 +59,21 @@ export const IconQuery = ({style}: { style?: React.CSSProperties }) => (
<rect x="0" y="0" width="15" height="15" rx="0"/>
</clipPath>
</defs>
<g clip-path="url(#master_svg0_1_3654/1_0572)">
<g clipPath="url(#master_svg0_1_3654/1_0572)">
<g>
<path
d="M8.4375,8.20313Q8.4375,8.2607,8.43186,8.31799Q8.426210000000001,8.37528,8.41498,8.431750000000001Q8.40375,8.48821,8.387039999999999,8.5433Q8.37033,8.59839,8.3483,8.65158Q8.32626,8.70477,8.29913,8.75554Q8.27199,8.80632,8.24,8.85418Q8.20802,8.90205,8.1715,8.94655Q8.13497,8.991060000000001,8.09427,9.03177Q8.053560000000001,9.07247,8.00905,9.109Q7.96455,9.14552,7.9166799999999995,9.1775Q7.8688199999999995,9.20949,7.81804,9.23663Q7.76727,9.26376,7.71408,9.2858Q7.66089,9.30783,7.6058,9.324539999999999Q7.5507100000000005,9.34125,7.49425,9.35248Q7.43778,9.363710000000001,7.38049,9.36936Q7.3232,9.375,7.26563,9.375Q7.20805,9.375,7.15076,9.36936Q7.093468,9.363710000000001,7.037003,9.35248Q6.980539,9.34125,6.925448,9.324539999999999Q6.870356,9.30783,6.817168,9.2858Q6.76398,9.26376,6.713207,9.23663Q6.662434,9.20949,6.614566,9.1775Q6.566698,9.14552,6.522195,9.109Q6.477693,9.07247,6.436984,9.03177Q6.396276,8.991060000000001,6.3597529999999995,8.94655Q6.323231,8.90205,6.291247,8.85418Q6.259262,8.80632,6.232123,8.75554Q6.204985,8.70477,6.1829537,8.65158Q6.1609224,8.59839,6.1442105,8.5433Q6.1274987,8.48821,6.1162672,8.431750000000001Q6.1050358,8.37528,6.0993929,8.31799Q6.09375,8.2607,6.09375,8.20313Q6.09375,8.14555,6.0993929,8.08826Q6.1050358,8.030968,6.1162672,7.974503Q6.1274987,7.918039,6.1442105,7.862948Q6.1609224,7.807856,6.1829537,7.754668Q6.204985,7.70148,6.232123,7.650707Q6.259262,7.599934,6.291247,7.552066Q6.323231,7.504198,6.3597529999999995,7.459695Q6.396276,7.415193,6.436984,7.374484Q6.477693,7.333776,6.522195,7.2972529999999995Q6.566698,7.260731,6.614566,7.228747Q6.662434,7.196762,6.713207,7.169623Q6.76398,7.142485,6.817168,7.1204537Q6.870356,7.0984224,6.925448,7.0817105Q6.980539,7.0649987,7.037003,7.0537672Q7.093468,7.0425358,7.15076,7.0368929Q7.20805,7.03125,7.26563,7.03125Q7.3232,7.03125,7.38049,7.0368929Q7.43778,7.0425358,7.49425,7.0537672Q7.5507100000000005,7.0649987,7.6058,7.0817105Q7.66089,7.0984224,7.71408,7.1204537Q7.76727,7.142485,7.81804,7.169623Q7.8688199999999995,7.196762,7.9166799999999995,7.228747Q7.96455,7.260731,8.00905,7.2972529999999995Q8.053560000000001,7.333776,8.09427,7.374484Q8.13497,7.415193,8.1715,7.459695Q8.20802,7.504198,8.24,7.552066Q8.27199,7.599934,8.29913,7.650707Q8.32626,7.70148,8.3483,7.754668Q8.37033,7.807856,8.387039999999999,7.862948Q8.40375,7.918039,8.41498,7.974503Q8.426210000000001,8.030968,8.43186,8.08826Q8.4375,8.14555,8.4375,8.20313Z"
fill="currentColor" fill-opacity="1"/>
fill="currentColor"/>
</g>
<g>
<path
d="M3.28125,0.9375C2.5046049999999997,0.9375,1.875,1.567105,1.875,2.34375L1.875,12.6563C1.875,13.4329,2.5046049999999997,14.0625,3.28125,14.0625L11.71875,14.0625C12.4954,14.0625,13.125,13.4329,13.125,12.6563L13.125,5.15622L10.3125,5.15622C9.53585,5.15622,8.90625,4.526619999999999,8.90625,3.74997L8.90625,0.9375L3.28125,0.9375ZM9.375,8.20312C9.375,8.61589,9.25644,9.00097,9.05152,9.32616L10.32722,10.60187L9.66431,11.2648L8.38859,9.98906C8.06342,10.19396,7.67836,10.3125,7.26562,10.3125C6.10065,10.3125,5.15625,9.3681,5.15625,8.20312C5.15625,7.03815,6.10065,6.09375,7.26562,6.09375C8.4306,6.09375,9.375,7.03815,9.375,8.20312Z"
fill="currentColor" fill-opacity="1"/>
fill="currentColor"/>
</g>
<g>
<path
d="M12.84958,3.55950103515625C13.02483,3.73431103515625,13.1238,3.97131103515625,13.12499,4.21872103515625L10.3125,4.21872103515625C10.053619,4.21872103515625,9.84375,4.008851035156249,9.84375,3.74997103515625L9.84375,0.93756103515625C10.088203,0.94027833515625,10.322109,1.03840803515625,10.495386,1.21125403515625L12.84958,3.55950103515625Z"
fill="currentColor" fill-opacity="1"/>
fill="currentColor"/>
</g>
</g>
</svg>
@ -87,21 +87,21 @@ export const IconReconciliation = ({style}: { style?: React.CSSProperties }) =>
<rect x="0" y="0" width="15" height="15" rx="6"/>
</clipPath>
</defs>
<g clip-path="url(#master_svg0_1_3654/1_0566)">
<g clipPath="url(#master_svg0_1_3654/1_0566)">
<g>
<path
d="M2.81335,5.56129C2.8471,4.29633,3.88314,3.28125,5.15625,3.28125C5.34382,3.28125,5.52626,3.30328,5.70108,3.34491C6.28865,2.459048,7.29486,1.875,8.4375,1.875C10.04117,1.875,11.3761,3.0254399999999997,11.6621,4.54604C13.0745,5.15864,14.0625,6.5655,14.0625,8.20312C14.0625,9.294170000000001,13.624,10.28278,12.9136,11.00232C13.1438,10.2376,12.5883,9.3752,11.6976,9.3752L11.247,9.3752L11.247,7.03119C11.247,6.51343,10.82728,6.09369,10.30951,6.09369L9.37201,6.09369C9.13329,6.09369,8.91541,6.18292,8.74989,6.32984C8.19456,5.96552,7.39427,5.99146,6.88692,6.60201L5.15099,8.69101C4.46064,9.52176,5.05144,10.78088,6.1316,10.78087L6.56222,10.78087L6.56222,12.1875L4.45312,12.1875C2.5115,12.1875,0.9375,10.6135,0.9375,8.67188C0.9375,7.32245,1.697769,6.1506,2.81335,5.56129Z"
fill="currentColor" fill-opacity="1"/>
fill="currentColor"/>
</g>
<g>
<path
d="M10.20127127734375,12.95489896484375C9.92087127734375,13.29233896484375,9.37200927734375,13.09405896484375,9.37200927734375,12.65530896484375L9.37200927734375,7.03118896484375L10.30950927734375,7.03118896484375L10.30950927734375,10.31269896484375L11.69760927734375,10.31269896484375C11.975589277343751,10.31269896484375,12.12763927734375,10.63672896484375,11.949969277343751,10.85053896484375L10.20127127734375,12.95489896484375Z"
fill="currentColor" fill-opacity="1"/>
fill="currentColor"/>
</g>
<g>
<path
d="M8.43601703125,7.46545189453125C8.43681703125,7.47703889453125,8.43721703125,7.48880089453125,8.43721703125,7.50075389453125L8.43721703125,13.124936894531249L7.49971703125,13.124936894531249L7.49971703125,9.84335689453125L6.13159703125,9.84335689453125C5.84567423125,9.84335689453125,5.68928803125,9.51007689453125,5.87202693125,9.29016689453125L7.60795703125,7.20116489453125C7.87685703125,6.87756589453125,8.39263703125,7.04663799453125,8.43450703125,7.44799089453125C8.43510703125,7.45376189453125,8.43560703125,7.45957789453125,8.43601703125,7.46545189453125Z"
fill="currentColor" fill-opacity="1"/>
fill="currentColor"/>
</g>
</g>
</svg>

View File

@ -0,0 +1,11 @@
import React from "react";
type MoneyFormatProps = {
money:number|string;
currency?:'USD'|'HKD'|'RMB';
}
const MoneyFormat:React.FC<MoneyFormatProps> = ({money,currency = 'HKD'})=>{
// 将货币数字转换为千分位格式且带2位小数
return <span>{Number(money).toLocaleString('zh-HK', {style: 'currency', currency})}</span>
}
export default MoneyFormat;

View File

@ -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',
}
}

View File

@ -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"
}
}

View File

@ -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": "请填写学号"
}
}

View File

@ -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": "请填写学号"
}
}

View File

@ -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 (<Space>
<Button size={'small'} theme={'solid'} type={'primary'}></Button>
<Button size={'small'} theme={'solid'} type={'primary'}></Button>
<Button onClick={()=>GeneratePdf('111.pdf')} size={'small'} theme={'solid'} type={'primary'}></Button>
</Space>)
}
return (<div>
<h1>BillQuery</h1>
<BillList type={'query'} operationRender={operation} />
</div>)
}
export default BillQuery

View File

@ -1,7 +1,10 @@
import {BillList} from "@/components/bill/list.tsx";
const BillReconciliation = () => {
return (<div>
<h1>BillQuery</h1>
<BillList type={'reconciliation'} onRowSelection={(keys)=>{
console.log('xxx',keys)
}} />
</div>)
}
export default BillReconciliation

View File

@ -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 <div className={styles.billDetailItem}>
<div className={styles.billDetailItemTitle}>{item.title} :</div>
<div className={styles.billDetailItemValue}>{item.value}</div>
</div>
}
export default function Index() {
return (<div>
<h1>Index</h1>
<Button onClick={()=>GeneratePdf('test-11.pdf')}>PDF</Button>
const {t} = useTranslation()
const qrCodeRef = useRef<HTMLDivElement>(null)
const downloadQRCode = ()=>{
const canvas = qrCodeRef.current?.querySelector('canvas');
if(!canvas) return
saveAs(canvas.toDataURL(), 'qrcode.png')
}
return (<div className={styles.manualPage}>
<div className={styles.generateForm}>
<Form layout='horizontal' onValueChange={values => console.log(values)}>
<Form.Select
field="bill_type" style={{width: 176}}
label={t('manual.bill_type')}
rules={[{required: true, message: t('manual.bill_type_required')}]}
>
<Form.Select.Option value="admin">TUITION FEE</Form.Select.Option>
<Form.Select.Option value="user">CAUTION FEE</Form.Select.Option>
<Form.Select.Option value="guest">APPLICATION FEE</Form.Select.Option>
</Form.Select>
<Form.Input
field='amount' label={t('manual.amount')} style={{minWidth: 120}}
rules={[{required: true, message: t('manual.amount_required')}]}
/>
<Form.Input
field='student_number' label={t('manual.student_number')} style={{minWidth: 150}}
rules={[{required: true, message: t('manual.student_number_required')}]}
/>
<div className={styles.generateButtonContainer}>
<Button htmlType="submit">{t('manual.btn_generate')}</Button>
</div>
</Form>
</div>
<Space className={styles.billDetail} align={'start'}>
<div >
<BillDetailItem title={t('manual.bill_type')} value={'TUITION FEE'} />
<BillDetailItem title={t('manual.student_number')} value={'12345612'} />
<BillDetailItem title={t('manual.amount')} value={'HK$ 13600.00'} />
<Button onClick={downloadQRCode} style={{marginTop:20}} theme={'solid'} type={'primary'}>Download QR code</Button>
</div>
<div className={styles.billQrCode}>
<div className={styles.QRCodeContainer}>
<div className={styles.qrCode} ref={qrCodeRef}>
<QRCode size={250} value={'http://localhost:5173/pay?bill=123123123&from=qrcode'} />
</div>
</div>
<div className={styles.billExpTime}> {t('manual.exp_time')} {'12:00'} </div>
</div>
</Space>
</div>)
}

View File

@ -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;
}

View File

@ -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}
</Typography.Title>
{process.env.NODE_ENV == 'development' && <div>
<pre style={{font: 'inherit',textAlign:'left',maxWidth:700,marginTop:20,maxHeight:300,overflow:'auto'}}>
<code style={{wordBreak:'break-all',font: 'inherit',whiteSpace:'break-spaces'}}>{error.stack}</code>
<pre style={{
font: 'inherit',
textAlign: 'left',
maxWidth: 700,
marginTop: 20,
maxHeight: 300,
overflow: 'auto'
}}>
<code style={{
wordBreak: 'break-all',
font: 'inherit',
whiteSpace: 'break-spaces'
}}>{error.stack}</code>
</pre>
</div>}
<Stack sx={{mt: 5}} direction={'row'} spacing={2}>

View File

@ -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 (<Dropdown
position={'bottomRight'}
trigger={'click'}
render={
<Dropdown.Menu>
<Dropdown.Item icon={<IconExit />} onClick={logout}>{t('layout.logout')}</Dropdown.Item>
</Dropdown.Menu>
<>
<HeaderUserProfile>
<Space>
<Avatar color="orange" size="small">{user?.nickname}</Avatar>
<div>
<Typography.Title heading={6}>{user?.nickname}</Typography.Title>
<Typography.Text type="quaternary"
size={'small'}>DEPT:{user?.department?.toUpperCase()}</Typography.Text>
</div>
</Space>
</HeaderUserProfile>
<Dropdown.Menu>
<Dropdown.Divider/>
<Dropdown.Item icon={<IconExit/>} onClick={logout}>{t('layout.logout')}</Dropdown.Item>
</Dropdown.Menu>
</>
}
>
<Avatar color="orange" size="small">{user?.nickname}</Avatar>
</Dropdown>)
}
export const CommonHeader: React.FC<CommonHeaderProps> = ({children, title, rightExtra}) => {
return (<Header style={{backgroundColor: 'var(--semi-color-bg-1)', position: 'sticky', top: 0, zIndex: 100}}>
const {appName} = useConfig()
return (<Header style={{position: 'sticky', top: 0, zIndex: 100}}>
<div>
<Nav mode="horizontal" defaultSelectedKeys={['Home']}>
<Nav mode="horizontal" defaultSelectedKeys={['Home']}
style={{backgroundColor: '#43ABFF', color: '#fff', height: "var(--dashboard-header-height,50px)"}}>
<Nav.Header>
<AppLogo style={{fontSize: 16}}/>
<Space>
<AppLogo style={{fontSize: 12, color: 'white'}} theme={'color'}/>
{appName}
</Space>
</Nav.Header>
<div className={'align-center'} style={{color: 'var(--semi-color-text-2)',}}>
<span
@ -67,32 +95,39 @@ type LayoutProps = {
children: React.ReactNode
}
const LayoutContentContainer = styled.div({
backgroundColor: '#fff',
borderRadius: 10,
padding: 'var(--dashboard-layout-padding)',
marginTop:20
})
export const BaseLayout: React.FC<LayoutProps> = ({children}) => {
const {t} = useTranslation()
// get current route path to display menu name
const location = useLocation();
const currentRoutePath = useMemo(() => {
const nav = AllDashboardMenu.find(it => it.path == location.pathname)
return nav?.key
}, [location.pathname])
return (<Layout>
<CommonHeader/>
<Layout style={{
backgroundColor: '#f8f9fa'
backgroundColor: '#f8f9fa',
minHeight: 'calc(100vh - var(--dashboard-header-height,50px))',
}}>
<Sider style={{width: '250px'}}>
<DashboardNavigation />
<Sider style={{minWidth: 'var(--dashboard-navigation-width,250px)'}}>
<DashboardNavigation/>
</Sider>
<Content style={{padding: '15px'}}>
<div className="content-container" style={{backgroundColor: '#fff', minHeight: 300, borderRadius: 10,padding:15}}>
<Content style={{padding: 'var(--dashboard-layout-padding)'}}>
<Typography.Title>
{currentRoutePath ? t(`layout.menu.${currentRoutePath}`).toUpperCase() : ''}
</Typography.Title>
<LayoutContentContainer className="content-container">
{children}
</div>
</LayoutContentContainer>
</Content>
</Layout>
{/*<Footer*/}
{/* style={{*/}
{/* textAlign: 'center',*/}
{/* padding: '20px',*/}
{/* color: 'var(--semi-color-text-2)',*/}
{/* backgroundColor: 'rgba(var(--semi-color-bg-0), 1)',*/}
{/* }}*/}
{/*>*/}
{/* <span>Copyright © 2024 星图比特. All Rights Reserved. </span>*/}
{/*</Footer>*/}
</Layout>)
}

View File

@ -5,7 +5,7 @@ import {useTranslation} from "react-i18next";
import useAuth from "@/hooks/useAuth.ts";
import {IconQRCode, IconQuery, IconReconciliation} from "@/components/logo";
const AllDashboardMenu = [
export const AllDashboardMenu = [
{
key: 'manual',
icon: <IconQRCode/>,

34
src/types/bill.d.ts vendored Normal file
View File

@ -0,0 +1,34 @@
declare type BillDetail = {
id: string | number;
bill_id: string | number;
type: string;
amount: number;
}
declare type BillModel = {
id: string | number;
student_no: string;
application_no: string;
student_sc_name: string;
student_tc_name: string;
student_en_name: string;
program_id: string | number;
department: string;
student_year: string;
student_semester: string;
amount: number;
service_charge: number;
pay_amount: number;
actual_payment_amount: number;
pay_method: string;
currency: string;
payment_channel: string;
expiration_time: string | Date | number;
bill_status: string;
apply_status: string;
paid_area: string;
paid_at: string | Date | number;
created_at: string | Date | number;
updated_at: string | Date | number;
detail: BillDetail[]
}

11
src/types/core.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
declare type RecordList<T> = {
list: T[];
pagination: {
total: number;
pageSize: number;
current: number;
recordTotal?: number;
sort?: string;
filter?: string;
};
}

View File

@ -826,6 +826,11 @@
resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
"@types/file-saver@^2.0.7":
version "2.0.7"
resolved "https://registry.npmmirror.com/@types/file-saver/-/file-saver-2.0.7.tgz#8dbb2f24bdc7486c54aa854eb414940bbd056f7d"
integrity sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==
"@types/lodash@^4.17.1":
version "4.17.4"
resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.4.tgz#0303b64958ee070059e3a7184048a55159fe20b7"
@ -1261,7 +1266,7 @@ date-fns@^2.29.3:
dependencies:
"@babel/runtime" "^7.21.0"
dayjs@^1.9.1:
dayjs@^1.11.11, dayjs@^1.9.1:
version "1.11.11"
resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e"
integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==
@ -1510,6 +1515,11 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"
file-saver@^2.0.5:
version "2.0.5"
resolved "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@ -2108,6 +2118,11 @@ punycode@^2.1.0:
resolved "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
qrcode.react@^3.1.0:
version "3.1.0"
resolved "https://registry.npmmirror.com/qrcode.react/-/qrcode.react-3.1.0.tgz#5c91ddc0340f768316fbdb8fff2765134c2aecd8"
integrity sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"