✨ add bill list component
This commit is contained in:
parent
44095afbc7
commit
295d6c75e9
@ -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>
|
||||
|
@ -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
@ -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;
|
||||
|
187
src/components/bill/list.tsx
Normal file
187
src/components/bill/list.tsx
Normal 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>
|
||||
}
|
@ -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>
|
||||
}
|
@ -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>
|
||||
|
11
src/components/money-format.tsx
Normal file
11
src/components/money-format.tsx
Normal 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;
|
@ -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',
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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": "请填写学号"
|
||||
}
|
||||
}
|
@ -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": "请填写学号"
|
||||
}
|
||||
}
|
@ -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
|
@ -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
|
@ -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>)
|
||||
}
|
43
src/pages/manual/manual.module.less
Normal file
43
src/pages/manual/manual.module.less
Normal 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;
|
||||
}
|
@ -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}>
|
||||
|
@ -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>)
|
||||
}
|
||||
|
||||
|
@ -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
34
src/types/bill.d.ts
vendored
Normal 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
11
src/types/core.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
declare type RecordList<T> = {
|
||||
list: T[];
|
||||
pagination: {
|
||||
total: number;
|
||||
pageSize: number;
|
||||
current: number;
|
||||
recordTotal?: number;
|
||||
sort?: string;
|
||||
filter?: string;
|
||||
};
|
||||
}
|
17
yarn.lock
17
yarn.lock
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user