add role switch only for test
This commit is contained in:
parent
a4e21ec909
commit
d49ce2b815
44
Dockerfile-prod
Normal file
44
Dockerfile-prod
Normal 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;"]
|
@ -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"
|
||||
|
@ -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>)
|
||||
|
||||
|
@ -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
|
@ -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>
|
||||
|
@ -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...",
|
||||
|
13
src/main.tsx
13
src/main.tsx
@ -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>,
|
||||
|
69
src/pages/bill/external_create.tsx
Normal file
69
src/pages/bill/external_create.tsx
Normal 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
|
@ -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()
|
||||
|
@ -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})
|
||||
|
@ -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}>
|
||||
|
@ -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/>
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -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
1
src/types/auth.d.ts
vendored
@ -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
6
src/types/bill.d.ts
vendored
@ -80,4 +80,8 @@ declare type AsiaPayModel = {
|
||||
payMethod: string;
|
||||
pay_url: string;
|
||||
details: BillDetail[]
|
||||
}
|
||||
}
|
||||
|
||||
type ExternalCreateParamsType = {
|
||||
[key: string]: string | null | BillDetail[];
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user