add role switch only for test

This commit is contained in:
LittleBoy 2024-06-24 22:10:14 +08:00
parent a4e21ec909
commit d49ce2b815
17 changed files with 207 additions and 48 deletions

44
Dockerfile-prod Normal file
View File

@ -0,0 +1,44 @@
# Dockerfile for a React app build and run
FROM node:18.19.1-alpine AS builder
MAINTAINER yaclty2@gmail.com
WORKDIR /app
# envs 配置
# 应用部署后的URL
ENV APP_SITE_URL ""
# 应用接口前缀
ENV APP_API_URL ""
# Copy source code to the builder
COPY package.json yarn.lock* ./
COPY public ./public
COPY src ./src
COPY index.html ./
COPY *.ts .
COPY *.json .
# Install dependencies and build the app
RUN yarn install
RUN yarn build-prod
# Step 2. Production image, copy all the files and run nginx
FROM nginx:1.26-alpine3.19 AS runner
WORKDIR /app
# envs 配置
ENV APP_API_URL localhost:50000
# nginx配置文件
COPY nginx.conf /etc/nginx/conf.d/default.conf.template
# 编译文件
COPY --from=builder /app/dist ./
# RUN /bin/sh envsubst /etc/nginx/templates/*.template /etc/nginx/conf.d
WORKDIR /etc/nginx/conf.d/
ENTRYPOINT sed -i "s~<!--app_url-->~<script>const APP_SITE_URL='${APP_SITE_URL}';</script>~" /app/index.html && envsubst '$APP_API_URL' < default.conf.template > default.conf && cat default.conf && nginx -g 'daemon off;'
# 暴露80端口
EXPOSE 80
# 启动Nginx服务
# CMD ["nginx", "-g", "daemon off;"]

View File

@ -7,7 +7,8 @@
"dev": "vite --host",
"build": "tsc && vite build",
"build-test": "tsc && vite build --mode=test",
"build-docker:latest": "docker build -t payment-front:latest .",
"build-for-wm": "tsc && vite build --mode=for-wm",
"build-prod": "tsc && vite build --mode=production",
"clean-build": "rm -rf dist",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"

View File

@ -94,7 +94,7 @@ export const IconStudentEmail = ({style}: IconProps) => {
width="1em" height="1em" style={style}>
<path
d="M926.47619 355.644952V780.190476a73.142857 73.142857 0 0 1-73.142857 73.142857H170.666667a73.142857 73.142857 0 0 1-73.142857-73.142857V355.644952l73.142857 62.000762V780.190476h682.666666V417.645714l73.142857-62.000762zM853.333333 170.666667a74.044952 74.044952 0 0 1 26.087619 4.778666 72.704 72.704 0 0 1 30.622477 22.186667 73.508571 73.508571 0 0 1 10.678857 17.67619c3.169524 7.509333 5.12 15.652571 5.607619 24.210286L926.47619 243.809524v24.380952L559.469714 581.241905a73.142857 73.142857 0 0 1-91.306666 2.901333l-3.632762-2.925714L97.52381 268.190476v-24.380952a72.899048 72.899048 0 0 1 40.155428-65.292191A72.97219 72.97219 0 0 1 170.666667 170.666667h682.666666z m-10.971428 73.142857H181.638095L512 525.58019 842.361905 243.809524z"
fill="#00C479" />
fill="#00C479"/>
</svg>)
}
@ -132,12 +132,27 @@ export const IconBillType = ({style}: IconProps) => {
)
}
export const IconLoading = ({size}:{size?:string|number})=>(<svg xmlns="http://www.w3.org/2000/svg" style={{
margin: 'auto', display: 'block',...(size?{fontSize:size}:{})
}} width="1em" height="1em" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
export const IconLoading = ({size,color}: { size?: string | number,color?:string; }) => (<svg xmlns="http://www.w3.org/2000/svg" style={{
margin: 'auto', display: 'block', ...(size ? {fontSize: size} : {}), ...(color ? {color} : {})
}} width="1em" height="1em" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="50" cy="50" r="30" stroke="#6a6a6a" strokeWidth="6" fill="none"></circle>
<circle cx="50" cy="50" r="30" stroke="#aaaaaa" strokeWidth="6" strokeLinecap="round" fill="none">
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;180 50 50;720 50 50" keyTimes="0;0.5;1"></animateTransform>
<animate attributeName="stroke-dasharray" repeatCount="indefinite" dur="1s" values="18.84955592153876 169.64600329384882;94.2477796076938 94.24777960769377;18.84955592153876 169.64600329384882" keyTimes="0;0.5;1"></animate>
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s"
values="0 50 50;180 50 50;720 50 50" keyTimes="0;0.5;1"></animateTransform>
<animate attributeName="stroke-dasharray" repeatCount="indefinite" dur="1s"
values="18.84955592153876 169.64600329384882;94.2477796076938 94.24777960769377;18.84955592153876 169.64600329384882"
keyTimes="0;0.5;1"></animate>
</circle>
</svg>)
</svg>)
export const IconRoles = ({size,color}: { size?: string | number,color?:string; }) => (<svg xmlns="http://www.w3.org/2000/svg" style={{
margin: 'auto', display: 'block', ...(size ? {fontSize: size} : {}), ...(color ? {color} : {})
}} width="1em" height="1em" viewBox="0 0 1024 1024" preserveAspectRatio="xMidYMid">
<path
d="M559 470.9h137c26.9 0 48.6-22 47-48.8-1.5-24-21.6-43.2-45.9-43.2H560c-26.9 0-48.6 22-47 48.8 1.5 24 21.6 43.2 46 43.2M779.2 553.1H557.3c-24.4 0-44.4 20.7-44.4 46s20 46 44.4 46h221.9c24.4 0 44.4-20.7 44.4-46s-20-46-44.4-46M379 445.8c0-37.9-29-67-67-67-37.9 0-67 29-67 67s29 67 67 67 67-29 67-67M312 512.8c-62.5 0-111.6 42.4-111.6 93.8s223.2 51.3 223.2 0-49.1-93.8-111.6-93.8"
fill={'currentColor'}></path>
<path
d="M966.6 97.8H57.4c-31.6 0-57.4 25.7-57.4 57v714.4c0 31.4 25.8 57 57.4 57h909.3c31.6 0 57.4-25.7 57.4-57V154.8c-0.1-31.4-25.9-57-57.5-57zM932 834.2H92V189.8h840v644.4z"
fill={'currentColor'}></path>
</svg>)

View File

@ -117,6 +117,20 @@ export const AuthProvider = ({children}: { children: React.ReactNode }) => {
}
})
}
const updateUser = async (user: Partial<UserProfile>) => {
dispatch({
action: 'updateUser',
payload: {
user:{
...state.user,
...user
} as any,
}
})
};
useEffect(() => {
init().then(console.log)
}, [])
@ -128,7 +142,7 @@ export const AuthProvider = ({children}: { children: React.ReactNode }) => {
return (<AuthContext.Provider value={{
...state,
login, logout,
mockLogin
mockLogin,updateUser
}}>{children}</AuthContext.Provider>)
}
export default AuthContext

View File

@ -36,9 +36,9 @@ export const I18nSwitcher = () => {
</Dropdown.Menu>
}
>
<Button theme="borderless">
<Space align={'center'} spacing={2} style={{marginTop:2}}>
<IconLanguage style={{ color: '#666' }} />
<Button theme="borderless" style={{marginRight:3}}>
<Space align={'center'} spacing={2} style={{transform:'translateY(3px)'}}>
<IconLanguage style={{ color: '#fff' }} />
{/* {LocaleList.find(s => s.key == locale)?.text}*/}
</Space>
</Button>

View File

@ -84,7 +84,7 @@
},
"pay": {
"amount": "Amount",
"bill_error": "Bills to be paid do not exist or are overdue",
"bill_error": "The bill to be paid does not exist or is overdue",
"charge": "Charge",
"confirm_pay": "CONFIRM PAYMENT",
"query_pay_status": "Check payment status...",

View File

@ -1,22 +1,9 @@
// import React from "react";
import ReactDOM from 'react-dom/client'
import * as Sentry from '@sentry/react';
import App from './App.tsx'
import '@/assets/index.less'
Sentry.init({
dsn: "https://571faf95edca40ac981a573eff1b17fe@logs.1688cd.cn/1",
//dsn: "https://5224dbb04d4a6a521edeb67552310836@o4505757500309504.ingest.us.sentry.io/4507373697564672",
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration(),
],
tracesSampleRate: 1.0,
tracePropagationTargets: ["localhost", ], // /^https:\/\/yourserver\.io\/api/,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});
ReactDOM.createRoot(document.getElementById('root')!).render(
<App/>,
// <React.StrictMode></React.StrictMode>,

View File

@ -0,0 +1,69 @@
import styles from "@/pages/pay/pay.module.less";
import {useNavigate, useSearchParams} from "react-router-dom";
import {useSetState} from "ahooks";
import {useEffect} from "react";
import {createExternalBill} from "@/service/api/bill.ts";
import {IconLoading} from "@/components/icons";
// 获取必填参数
const RequiredParams = [
'application_number', // 'student_number', 可以不设置
'source', 'amount', 'student_chinese_name',
'student_english_name', 'student_email',
'program_code', 'programme_chinese_name',
'programme_english_name', 'department_chinese_name',
'department_english_name', 'attendance_mode',
'intake_year', 'intake_semester'
]
const ExternalCreate = () => {
const [state, setState] = useSetState<{
error?: string;
params?: ExternalCreateParamsType;
loading?: boolean;
}>({
loading: true
})
const [searchParams] = useSearchParams();
const navigate = useNavigate()
const createBill = (params: ExternalCreateParamsType) => {
setState({loading: true})
createExternalBill(params).then((ret) => {
setState({loading: false})
navigate(`/pay?bill=${ret.id}`, {replace: true})
}).catch(() => {
setState({loading: false, 'error': 'create pay order error'})
})
}
useEffect(() => {
if (searchParams) {
const paramsContent = searchParams.get('params');
if (!paramsContent) {
return setState({error: 'params error'})
}
const params: ExternalCreateParamsType = JSON.parse(paramsContent);
for (let i = 0; i < RequiredParams.length; i++) {
const key = RequiredParams[i];
if (!params[key]) {
return setState({error: 'params error: require ' + key})
}
params[key] = searchParams.get(key)
}
if (!params.details || params.details.length == 0) {
return setState({error: 'params error: require detail'})
}
createBill(params)
return;
}
}, [searchParams])
return (<div className={`${styles.container} text-center`}>
{state.loading && <div>
<div><IconLoading size={70} /></div>
<div></div>
</div>}
{state.error && <div>
<h3>{state.error}</h3>
</div>}
</div>)
}
export default ExternalCreate

View File

@ -2,7 +2,6 @@ import {Button, Modal, Notification, Popconfirm, Space} from "@douyinfe/semi-ui"
import {useState} from "react";
import {useRequest} from "ahooks";
import {useTranslation} from "react-i18next";
import * as Sentry from "@sentry/react";
import {BillList} from "@/components/bill/list.tsx";
import SearchForm from "@/components/bill/search-form.tsx";
@ -29,13 +28,7 @@ const BillQuery = () => {
}), {
refreshDeps: [queryParams],
onError: (e) => {
Sentry.captureMessage('Error: Query bill error', {
level: 'error',
extra: {
message: e.message,
...queryParams
}
});
Notification.error({title: 'Error', content: e.message})
}
})
const {t} = useTranslation()

View File

@ -12,7 +12,6 @@ import {useSetState} from "ahooks";
import {BizError} from "@/service/types.ts";
import {IconAlertTriangle} from "@douyinfe/semi-icons";
import dayjs from "dayjs";
import * as Sentry from '@sentry/react'
export default function Index() {
@ -36,14 +35,6 @@ export default function Index() {
setBillInfo(ret)
//getBillDetail(ret.id).then(setBillInfo);
}).catch((e: BizError) => {
Sentry.captureMessage('Error: create manual bill error', {
level: 'error',
extra: {
message: e.message,
request_id: e.request_id,
...values
}
});
setState({errorMessage: e.message})
}).finally(() => {
setState({loading: false})

View File

@ -24,7 +24,7 @@ const PayIndex = () => {
useEffect(() => {
const billId = search.get('bill')
if (!billId || !/^\d+$/.test(billId)) {
navigate(`/fail?bill=${billId}`, {replace: true})
navigate(`/pay/error?bill=${billId}`, {replace: true})
return;
}
getBillDetail(Number(billId)).then((bill) => {
@ -34,7 +34,7 @@ const PayIndex = () => {
}
setBill(bill)
}).catch(() => {
navigate(`/fail?bill=${billId}&msg=bill_error`, {replace: true})
navigate(`/pay/error?bill=${billId}&msg=bill_error`, {replace: true})
})
}, [])
return (<div className={styles.container}>

View File

@ -20,6 +20,7 @@ import Loader from "@/components/loader.tsx";
import ManualIndex from "@/pages/manual/index.tsx";
import BillQuery from "@/pages/bill/query.tsx";
import BillReconciliation from "@/pages/bill/reconciliation.tsx";
import ExternalCreate from "@/pages/bill/external_create.tsx";
const routes: RouteObject[] = [
@ -48,6 +49,10 @@ const routes: RouteObject[] = [
path: 'pay/:result',
element: <PayResult/>
},
{
path: 'bill/external_create',
element: <ExternalCreate/>
},
]
},
{

View File

@ -1,6 +1,6 @@
import {Outlet, useLocation} from "react-router-dom";
import React, {useMemo} from "react";
import {Avatar, Dropdown, Layout, Nav, Space, Typography} from "@douyinfe/semi-ui"
import {Avatar, Button, Dropdown, Layout, Nav, Space, Typography} from "@douyinfe/semi-ui"
import {useTranslation} from "react-i18next";
import AuthGuard from "@/routes/layout/auth-guard.tsx";
@ -11,6 +11,7 @@ import {AllDashboardMenu, DashboardNavigation} from "@/routes/layout/dashboard-n
import {IconExit, IconUser} from "@douyinfe/semi-icons";
import styled from "@emotion/styled";
import useConfig from "@/hooks/useConfig.ts";
import {IconRoles} from "@/components/icons";
const {Header, Content, Sider} = Layout;
@ -56,8 +57,36 @@ export const HeaderUserAvatar = () => {
<Avatar color="orange" size="small"><IconUser /></Avatar>
</Dropdown>)
}
const RoleList = ['root', 'ro', 'fo','staff']
const RoleSwitcher = ()=>{
const {user, updateUser} = useAuth()
return (<>
{AppMode !== 'production' && (<Dropdown
clickToHide
render={
<Dropdown.Menu>
{RoleList.map((key) => (
<Dropdown.Item
active={user?.department == key} key={key}
onClick={() => updateUser({department: key})}
><span>{key.toUpperCase()}</span></Dropdown.Item>
))}
</Dropdown.Menu>
}
>
<Button theme="borderless">
<Space style={{transform:'translateY(3px)'}}>
<IconRoles size={20} color={'white'} />
<span style={{color:'white'}}>{user?.department?.toUpperCase()}</span>
</Space>
</Button>
</Dropdown>) }
</>)
}
export const CommonHeader: React.FC<CommonHeaderProps> = ({children, title, rightExtra}) => {
const {appName} = useConfig()
return (<Header style={{position: 'sticky', top: 0, zIndex: 100}}>
<div>
<Nav mode="horizontal" defaultSelectedKeys={['Home']}
@ -82,6 +111,7 @@ export const CommonHeader: React.FC<CommonHeaderProps> = ({children, title, righ
<Nav.Footer>
<Space>
{rightExtra}
<RoleSwitcher />
<I18nSwitcher/>
<HeaderUserAvatar/>
</Space>

View File

@ -58,3 +58,8 @@ export function confirmBills(bill_ids: number[]) {
export function updateBillPaymentSuccess(bill_id: number, merchant_ref: string, payment_channel: string) {
return post<BillModel>(`/bills/finish`, {merchant_ref, payment_channel, bill_id})
}
export function createExternalBill(params: ExternalCreateParamsType) {
return post<BillModel>('/bill', params)
}

1
src/types/auth.d.ts vendored
View File

@ -25,4 +25,5 @@ declare type AuthContextType = {
logout: () => Promise<void>;
mockLogin: () => Promise<void>;
login: (code:string,state:string) => Promise<void>;
updateUser: (user:Partial<UserProfile>) => Promise<void>;
};

6
src/types/bill.d.ts vendored
View File

@ -80,4 +80,8 @@ declare type AsiaPayModel = {
payMethod: string;
pay_url: string;
details: BillDetail[]
}
}
type ExternalCreateParamsType = {
[key: string]: string | null | BillDetail[];
}

View File

@ -7,7 +7,7 @@ export default defineConfig(({mode}) => {
return {
plugins: [react()],
base: mode == 'test' ? './' : '/',
base: mode == 'for-wm' ? './' : '/',
define: {
AppConfig: JSON.stringify({
SITE_URL: process.env.APP_SITE_URL || null,