[feat] Initial code

This commit is contained in:
JasonWu
2021-09-06 18:00:03 +08:00
commit 7dc01d5480
138 changed files with 48947 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
import { HeaderContent } from './types';
class Header {
public header: HeaderContent;
constructor() {
this.header = {};
}
updateToken(token: string): void {
this.header = {
...this.header,
Authorization: `Bearer ${token}`,
};
}
removeToken(): void {
if ('Authorization' in this.header) {
delete this.header.Authorization;
}
}
}
export default Header;
+5
View File
@@ -0,0 +1,5 @@
import axios, { AxiosInstance } from 'axios';
export const createAxios = ({ host }: { host: string }): AxiosInstance => axios.create({
baseURL: host,
});
+3
View File
@@ -0,0 +1,3 @@
export interface HeaderContent {
Authorization?: string;
}
+49
View File
@@ -0,0 +1,49 @@
import { AxiosInstance } from 'axios';
import { getIsJwtToken, getIsJwtTokenExpire } from '@Tools/utility';
import Header from './_APITool/Header';
import { createAxios } from './_APITool';
import { CreatUserAPI } from './user-apis';
import { UserAPIProps } from './user-apis/types';
import { APIParams } from './types';
class API {
private axios: AxiosInstance;
public baseHeader: Header;
public baseToken: string;
public user: UserAPIProps;
constructor({ host }: APIParams) {
this.axios = createAxios({ host });
this.baseHeader = new Header();
this.baseToken = '';
this.user = CreatUserAPI({ axios: this.axios, header: this.baseHeader });
}
updateAccessToken = (token: string): void => {
this.baseHeader.updateToken(token);
this.baseToken = token;
};
removeAccessToken = (): void => {
this.baseHeader.removeToken();
this.baseToken = '';
};
checkAccessTokenValid = async (): Promise<boolean> => {
const isJwtToken = await getIsJwtToken(this.baseToken);
return isJwtToken;
};
checkAccessTokenExpired = async (expiredTime = 0): Promise<boolean> => {
const isJwtToken = await this.checkAccessTokenValid();
if (isJwtToken) {
return getIsJwtTokenExpire(this.baseToken, expiredTime);
}
return true;
};
}
export default API;
+3
View File
@@ -0,0 +1,3 @@
export interface APIParams {
host: string;
}
+79
View File
@@ -0,0 +1,79 @@
import { AxiosInstance } from 'axios';
import Header from '../_APITool/Header';
import {
UserAPIProps,
PostUserRefreshAPIPromise,
GetUserSingleSignInAPIPromise,
GetUserAccountInfoAPIPromise,
PostUserSignOutAPIPromise,
} from './types';
export function CreatUserAPI({ axios, header }: { axios: AxiosInstance; header: Header }): UserAPIProps {
return {
getUserSSO: async (backUrl: string): Promise<GetUserSingleSignInAPIPromise> => {
try {
const res = await axios.get('/account/login/sso', {
params: {
back_url: backUrl,
},
});
return res.data;
} catch (error) {
const errorMessage = {
url: '',
error: error.response?.data,
} as GetUserSingleSignInAPIPromise;
return errorMessage;
}
},
postUserRefreshToken: async (): Promise<PostUserRefreshAPIPromise> => {
try {
const res = await axios.post('/account/refresh_token', undefined, {
headers: { ...header.header },
});
return res.data;
} catch (error) {
const errorMessage = {
token: '',
error: error.response?.data,
};
return errorMessage;
}
},
getUserAccountInfo: async (): Promise<GetUserAccountInfoAPIPromise> => {
try {
const res = await axios.get('/account', {
headers: { ...header.header },
});
return res.data;
} catch (error) {
const errorMessage = {
account: {
_id: '',
username: '',
email: '',
display_name: '',
createdAt: '',
updatedAt: '',
error: error.response?.data,
},
};
return errorMessage;
}
},
postUserSignOut: async (): Promise<PostUserSignOutAPIPromise> => {
try {
const res = await axios.post('/account/logout', undefined, {
headers: { ...header.header },
});
return res.data;
} catch (error) {
const errorMessage = {
url: '',
error: error.response?.data,
};
return errorMessage;
}
},
};
}
+27
View File
@@ -0,0 +1,27 @@
import { APIError } from '@Models/GeneralTypes';
import {
UserTokenInfo,
UserSignInInfo,
UserOAuthUrl,
UserSignOutUrl,
} from '@Models/Redux/User/types';
export interface GetUserSingleSignInAPIPromise extends UserOAuthUrl {
error?: APIError;
}
export interface PostUserRefreshAPIPromise extends UserTokenInfo {
error?: APIError;
}
export interface GetUserAccountInfoAPIPromise extends UserSignInInfo {
error?: APIError;
}
export interface PostUserSignOutAPIPromise extends UserSignOutUrl {
error?: APIError;
}
export interface UserAPIProps {
getUserSSO: (backUrl: string) => Promise<GetUserSingleSignInAPIPromise>;
postUserRefreshToken: () => Promise<PostUserRefreshAPIPromise>;
getUserAccountInfo: () => Promise<GetUserAccountInfoAPIPromise>;
postUserSignOut: () => Promise<PostUserSignOutAPIPromise>;
}
+42
View File
@@ -0,0 +1,42 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin:auto;background:#fff;display:block;" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<g>
<animateTransform attributeName="transform" type="rotate" values="360 50 50;0 50 50" keyTimes="0;1" dur="1s" repeatCount="indefinite" calcMode="spline" keySplines="0.5 0 0.5 1" begin="-0.1s"></animateTransform>
<circle cx="50" cy="50" r="39.891" stroke="#6994b7" stroke-width="14.4" fill="none" stroke-dasharray="0 300">
<animate attributeName="stroke-dasharray" values="15 300;55.1413599195142 300;15 300" keyTimes="0;0.5;1" dur="1s" repeatCount="indefinite" calcMode="linear" keySplines="0 0.4 0.6 1;0.4 0 1 0.6" begin="-0.046s"></animate>
</circle>
<circle cx="50" cy="50" r="39.891" stroke="#eeeeee" stroke-width="7.2" fill="none" stroke-dasharray="0 300">
<animate attributeName="stroke-dasharray" values="15 300;55.1413599195142 300;15 300" keyTimes="0;0.5;1" dur="1s" repeatCount="indefinite" calcMode="linear" keySplines="0 0.4 0.6 1;0.4 0 1 0.6" begin="-0.046s"></animate>
</circle>
<circle cx="50" cy="50" r="32.771" stroke="#000000" stroke-width="1" fill="none" stroke-dasharray="0 300">
<animate attributeName="stroke-dasharray" values="15 300;45.299378454348094 300;15 300" keyTimes="0;0.5;1" dur="1s" repeatCount="indefinite" calcMode="linear" keySplines="0 0.4 0.6 1;0.4 0 1 0.6" begin="-0.046s"></animate>
</circle>
<circle cx="50" cy="50" r="47.171" stroke="#000000" stroke-width="1" fill="none" stroke-dasharray="0 300">
<animate attributeName="stroke-dasharray" values="15 300;66.03388996804073 300;15 300" keyTimes="0;0.5;1" dur="1s" repeatCount="indefinite" calcMode="linear" keySplines="0 0.4 0.6 1;0.4 0 1 0.6" begin="-0.046s"></animate>
</circle>
</g>
<g>
<animateTransform attributeName="transform" type="rotate" values="360 50 50;0 50 50" keyTimes="0;1" dur="1s" repeatCount="indefinite" calcMode="spline" keySplines="0.5 0 0.5 1"></animateTransform>
<path fill="#6994b7" stroke="#000000" d="M97.2,50.1c0,6.1-1.2,12.2-3.5,17.9l-13.3-5.4c1.6-3.9,2.4-8.2,2.4-12.4"></path>
<path fill="#eeeeee" d="M93.5,49.9c0,1.2,0,2.7-0.1,3.9l-0.4,3.6c-0.4,2-2.3,3.3-4.1,2.8l-0.2-0.1c-1.8-0.5-3.1-2.3-2.7-3.9l0.4-3 c0.1-1,0.1-2.3,0.1-3.3"></path>
<path fill="#6994b7" stroke="#000000" d="M85.4,62.7c-0.2,0.7-0.5,1.4-0.8,2.1c-0.3,0.7-0.6,1.4-0.9,2c-0.6,1.1-2,1.4-3.2,0.8c-1.1-0.7-1.7-2-1.2-2.9 c0.3-0.6,0.5-1.2,0.8-1.8c0.2-0.6,0.6-1.2,0.7-1.8"></path>
<path fill="#6994b7" stroke="#000000" d="M94.5,65.8c-0.3,0.9-0.7,1.7-1,2.6c-0.4,0.9-0.7,1.7-1.1,2.5c-0.7,1.4-2.3,1.9-3.4,1.3h0 c-1.1-0.7-1.5-2.2-0.9-3.4c0.4-0.8,0.7-1.5,1-2.3c0.3-0.8,0.7-1.5,0.9-2.3"></path>
</g>
<g>
<animateTransform attributeName="transform" type="rotate" values="360 50 50;0 50 50" keyTimes="0;1" dur="1s" repeatCount="indefinite" calcMode="spline" keySplines="0.5 0 0.5 1" begin="-0.1s"></animateTransform>
<path fill="#eeeeee" stroke="#000000" d="M86.9,35.3l-6,2.4c-0.4-1.2-1.1-2.4-1.7-3.5c-0.2-0.5,0.3-1.1,0.9-1C82.3,33.8,84.8,34.4,86.9,35.3z"></path>
<path fill="#eeeeee" stroke="#000000" d="M87.1,35.3l6-2.4c-0.6-1.7-1.5-3.3-2.3-4.9c-0.3-0.7-1.2-0.6-1.4,0.1C88.8,30.6,88.2,33,87.1,35.3z"></path>
<path fill="#6994b7" stroke="#000000" d="M82.8,50.1c0-3.4-0.5-6.8-1.6-10c-0.2-0.8-0.4-1.5-0.3-2.3c0.1-0.8,0.4-1.6,0.7-2.4c0.7-1.5,1.9-3.1,3.7-4l0,0 c1.8-0.9,3.7-1.1,5.6-0.3c0.9,0.4,1.7,1,2.4,1.8c0.7,0.8,1.3,1.7,1.7,2.8c1.5,4.6,2.2,9.5,2.3,14.4"></path>
<path fill="#eeeeee" d="M86.3,50.2l0-0.9l-0.1-0.9l-0.1-1.9c0-0.9,0.2-1.7,0.7-2.3c0.5-0.7,1.3-1.2,2.3-1.4l0.3,0 c0.9-0.2,1.9,0,2.6,0.6c0.7,0.5,1.3,1.4,1.4,2.4l0.2,2.2l0.1,1.1l0,1.1"></path>
<path fill="#ff9922" d="M93.2,34.6c0.1,0.4-0.3,0.8-0.9,1c-0.6,0.2-1.2,0.1-1.4-0.2c-0.1-0.3,0.3-0.8,0.9-1 C92.4,34.2,93,34.3,93.2,34.6z"></path>
<path fill="#ff9922" d="M81.9,38.7c0.1,0.3,0.7,0.3,1.3,0.1c0.6-0.2,1-0.6,0.9-0.9c-0.1-0.3-0.7-0.3-1.3-0.1 C82.2,38,81.8,38.4,81.9,38.7z"></path>
<path fill="#000000" d="M88.5,36.8c0.1,0.3-0.2,0.7-0.6,0.8c-0.5,0.2-0.9,0-1.1-0.3c-0.1-0.3,0.2-0.7,0.6-0.8C87.9,36.3,88.4,36.4,88.5,36.8z"></path>
<path stroke="#000000" d="M85.9,38.9c0.2,0.6,0.8,0.9,1.4,0.7c0.6-0.2,0.9-0.9,0.6-2.1c0.3,1.2,1,1.7,1.6,1.5c0.6-0.2,0.9-0.8,0.8-1.4"></path>
<path fill="#6994b7" stroke="#000000" d="M86.8,42.3l0.4,2.2c0.1,0.4,0.1,0.7,0.2,1.1l0.1,1.1c0.1,1.2-0.9,2.3-2.2,2.3c-1.3,0-2.5-0.8-2.5-1.9l-0.1-1 c0-0.3-0.1-0.6-0.2-1l-0.3-1.9"></path>
<path fill="#6994b7" stroke="#000000" d="M96.2,40.3l0.5,2.7c0.1,0.5,0.2,0.9,0.2,1.4l0.1,1.4c0.1,1.5-0.9,2.8-2.2,2.9h0c-1.3,0-2.5-1.1-2.6-2.4 L92.1,45c0-0.4-0.1-0.8-0.2-1.2l-0.4-2.5"></path>
<path fill="#000000" d="M91.1,34.1c0.3,0.7,0,1.4-0.7,1.6c-0.6,0.2-1.3-0.1-1.6-0.7c-0.2-0.6,0-1.4,0.7-1.6C90.1,33.1,90.8,33.5,91.1,34.1z"></path>
<path fill="#000000" d="M85.5,36.3c0.2,0.6-0.1,1.2-0.7,1.5c-0.6,0.2-1.3,0-1.5-0.6C83,36.7,83.4,36,84,35.8C84.6,35.5,85.3,35.7,85.5,36.3z"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
<path d="M0 0h24v24H0z" fill="none" />
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" fill="#d0021b" />
</svg>

After

Width:  |  Height:  |  Size: 197 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
<path d="M0 0h24v24H0z" fill="none" />
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" fill="#393939" />
</svg>

After

Width:  |  Height:  |  Size: 197 B

+1
View File
@@ -0,0 +1 @@
export const ENTER_KEY_CODE = 13;
+32
View File
@@ -0,0 +1,32 @@
import BASE_URL from '../url';
import { RouteObject } from './types';
const ADMIN_URL = BASE_URL.ADMIN;
const Admin: RouteObject = {
main: [
{
name: 'Home',
route: ADMIN_URL.ROOT_PAGE_HOME,
lang: 'frontend.global.property.routehome',
component: import('@Components/Pages/Home'),
exact: false,
subRoute: [
{
name: 'Home',
route: `${ADMIN_URL.PAGE_HOME_OVERVIEW}`,
lang: '',
component: import('@Components/Pages/Home'),
exact: false,
subRoute: [],
avatar: '',
avatarActive: '',
},
],
avatar: 'route/disactive/home.svg',
avatarActive: 'route/active/home.svg',
},
],
};
export default Admin;
+29
View File
@@ -0,0 +1,29 @@
import BASE_URL from '../url';
import { RouteObject } from './types';
const GUEST_URL = BASE_URL.GUEST;
const Guest: RouteObject = {
registration: [
{
name: 'SignIn',
route: `${GUEST_URL.BASE_PAGE_SIGN_IN}`,
component: import('@Components/Pages/SignIn'),
exact: false,
subRoute: [],
avatar: '',
avatarActive: '',
},
{
name: 'OAuth',
route: `${GUEST_URL.BASE_PAGE_OAUTH}`,
component: import('@Components/Pages/OAuth'),
exact: false,
subRoute: [],
avatar: '',
avatarActive: '',
},
],
};
export default Guest;
+7
View File
@@ -0,0 +1,7 @@
import Guest from './Guest';
import Admin from './Admin';
export default {
Guest,
Admin,
};
+23
View File
@@ -0,0 +1,23 @@
import { LazyExoticComponent } from 'react';
import { EnvConfig } from '@Env/types';
import { ModuleKey } from '@Models/Redux/User/types';
export { ModuleKey };
export interface RouteItem {
name: string;
route: string;
lang?: string;
component: LazyExoticComponent;
subRoute: RouteItem[];
exact: boolean;
avatar: string;
avatarActive: string;
permissions?: ModuleKey[];
openNewWindow?: boolean;
openNewWindowKey?: (keyof EnvConfig)[];
}
export interface RouteObject {
[key: string]: RouteItem[];
}
+1
View File
@@ -0,0 +1 @@
export const TIMEZONE_TAIPEI = 'Asia/Taipei';
+3
View File
@@ -0,0 +1,3 @@
/* 首頁 */
export const ROOT_PAGE_HOME = '/home';
export const PAGE_HOME_OVERVIEW = '/overview';
+5
View File
@@ -0,0 +1,5 @@
/* 使用者登入 */
export const BASE_PAGE_SIGN_IN = '/login';
/* 第三方登入 */
export const BASE_PAGE_OAUTH = '/oauth_check';
+7
View File
@@ -0,0 +1,7 @@
import * as GUEST from './Guest';
import * as ADMIN from './Admin';
export default {
GUEST,
ADMIN,
};
+6
View File
@@ -0,0 +1,6 @@
/* Component Style */
:local(.appContainer) {
width: 100%;
height: 100%;
position: relative;
}
+29
View File
@@ -0,0 +1,29 @@
import React, { memo, useMemo } from 'react';
import useMappedState from '@Hooks/useMappedState';
import MainAppView from '@Components/Layer/MainAppView';
import MainAppCheckView from '@Components/Layer/MainAppCheckView';
import ModalDialog from '@Components/Common/Modals/ModalDialog';
import ModalConfirm from '@Components/Common/Modals/ModalConfirm';
import { AppProps } from './types';
const App: React.FC<AppProps> = ({ Router, routerProps }): React.ReactElement => {
/* Global & Local State */
const storeUser = useMappedState((state) => state.user);
/* Views */
const RenderMainView = useMemo(() => {
if (storeUser.userAuthCheck) {
return <MainAppView />;
}
return <MainAppCheckView />;
}, [storeUser.userAuthCheck]);
/* Main */
return (
<Router {...routerProps}>
{RenderMainView}
<ModalDialog />
<ModalConfirm />
</Router>
);
};
export default memo(App);
+8
View File
@@ -0,0 +1,8 @@
import React from 'react';
import { BrowserRouterProps } from 'react-router-dom';
import { StaticRouterProps } from 'react-router';
export interface AppProps {
Router: React.ComponentClass<Record<string, unknown>>;
routerProps?: BrowserRouterProps | StaticRouterProps;
}
@@ -0,0 +1,12 @@
/* Component Style */
:local(.gifLoaderContainer) {
width: 180px;
height: 180px;
position: relative;
}
:local(.gifLoaderStyle) {
width: 100%;
height: 100%;
position: relative;
object-fit: contain;
}
+15
View File
@@ -0,0 +1,15 @@
import React, { memo } from 'react';
import classNames from 'classnames';
import { loadImage } from '@Tools/image-loader';
import Styles from './index.module.css';
function GifLoader(): React.ReactElement {
/* Main */
return (
<div className={classNames(Styles.gifLoaderContainer)}>
<img className={classNames(Styles.gifLoaderStyle)} alt="dipp-gif-loading" src={loadImage('loading.svg')} />
</div>
);
}
export default memo(GifLoader);
+47
View File
@@ -0,0 +1,47 @@
import React, {
lazy, Suspense, useMemo, useState, useEffect,
} from 'react';
import cloneDeep from 'lodash/cloneDeep';
import Skeleton from '@material-ui/lab/Skeleton';
import Loading from '@Components/Base/Loading';
import { Props, ComponentState } from './types';
const Components: { [key: string]: ComponentState } = {};
const Lazy: React.FC<Props> = (props): React.ReactElement => {
const { componentImport, componentChunkName, componentProps } = props;
const [isRender, setIsRender] = useState(false);
const RenderComponent = useMemo(() => {
const Component = Components[componentChunkName];
if (Component) {
return <Component {...componentProps} />;
}
return (
<div style={{ width: '100%', height: '100%' }}>
<Loading typePosition="relative" typeZIndex={10005} typeIcon="line:fix" isLoading />
<Skeleton variant="rect" width="100%" height="100%" animation="wave" />
</div>
);
}, [componentChunkName, isRender, componentProps]);
/* Hooks */
useEffect(() => {
(async () => {
if (!Components[componentChunkName]) {
Components[componentChunkName] = lazy(async () => {
const Element = await componentImport;
return Element;
});
}
setIsRender(cloneDeep(!isRender));
})();
}, [componentChunkName]);
return (
<React.Fragment>
<Suspense fallback={<Loading typePosition="relative" typeZIndex={10005} typeIcon="line:fix" isLoading />}>
{RenderComponent}
</Suspense>
</React.Fragment>
);
};
export default Lazy;
+13
View File
@@ -0,0 +1,13 @@
import { ComponentClass, LazyExoticComponent } from 'react';
export type ComponentState = LazyExoticComponent | null;
export interface Props {
componentImport: Promise<{ default: LazyExoticComponent<ComponentClass> }>;
componentChunkName: string;
componentProps: { [key: string] };
}
export interface State {
Component: ComponentClass;
}
@@ -0,0 +1,110 @@
/* General Style */
:local(.flexCentral) {
display: flex;
justify-content: center;
align-items: center;
align-content: center;
flex-direction: column;
}
@keyframes iconLoadAnim {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
@keyframes textLoadAnim {
to {
width: 20px;
margin-right: 0;
}
}
/* Component Style */
:local(.fullScreenLoadingContainer) {
width: 100%;
height: 100%;
position: fixed;
top: 0px;
left: 0px;
z-index: 10000;
}
:local(.relateScreenLoadingContainer) {
width: 100%;
height: 100%;
position: absolute;
top: 0px;
left: 0px;
z-index: 10;
}
:local(.backgroundBlack) {
background: rgba(0, 0, 0, 0.6);
}
:local(.backgroundWhite) {
background: rgb(255, 255, 255, 0.9);
}
:local(.backgroundBlackImage) {
width: 100%;
height: 100%;
position: absolute;
background-size: cover;
}
:local(.backgroundWhiteImage) {
width: 100%;
height: 100%;
position: absolute;
background-size: cover;
}
:local(.loadingTextWhite) {
font-style: normal;
font-weight: bold;
font-size: 14px;
line-height: 20px;
color: #e02020;
}
:local(.loadingTextBlack) {
font-style: normal;
font-weight: bold;
font-size: 13px;
color: white;
}
:local(.loadingAreaContainer){
height: 70px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
:local(.loadingIconStyle) {
width: 100%;
height: 100%;
object-fit: contain;
animation: iconLoadAnim 1.2s infinite ease-in-out;
}
:local(.loadingTextStyle) {
width: 100%;
display: flex;
font-style: normal;
text-align: center;
margin-top: 10px;
}
:local(.loadingTextStyle::before){
font-style: normal;
text-align: center;
}
:local(.loadingTextStyle::after) {
overflow: hidden;
display: inline-block;
vertical-align: bottom;
animation: textLoadAnim steps(4, end) 1.2s infinite;
content: "\2026";
width: 0;
}
:local(.loadingZIndex) {
z-index: 1000;
}
+109
View File
@@ -0,0 +1,109 @@
import React, { memo, useMemo } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import LinearProgress from '@material-ui/core/LinearProgress';
import CircularProgress from '@material-ui/core/CircularProgress';
import { LoadingProps } from './types';
import Styles from './index.module.css';
function Loading(props: LoadingProps): React.ReactElement {
/* Global & Local States */
const {
typePosition, typeBackground, typeZIndex, typeIcon, isLoading, isHideText, text,
} = props;
/* Views */
const RenderPosition = useMemo(() => {
switch (typePosition) {
case 'relative':
return Styles.relateScreenLoadingContainer;
case 'absolute':
return Styles.fullScreenLoadingContainer;
default:
return Styles.fullScreenLoadingContainer;
}
}, [typePosition]);
const RenderBackground = useMemo(() => {
switch (typeBackground) {
case 'white':
return Styles.backgroundWhite;
case 'black':
return Styles.backgroundBlack;
default:
return '';
}
}, [typeBackground]);
const RenderTextColor = useMemo(() => {
switch (typeBackground) {
case 'white':
return Styles.loadingTextWhite;
case 'black':
return Styles.loadingTextBlack;
default:
return Styles.loadingTextWhite;
}
}, [typeBackground]);
const RenderText = useMemo(() => {
if (isHideText) {
return <React.Fragment />;
}
return <div className={classNames(RenderTextColor, Styles.loadingTextStyle)}>{text}</div>;
}, [isHideText, text, typeBackground]);
const RenderAnimation = useMemo(() => {
switch (typeIcon) {
case 'basic':
return (
<div className={`${Styles.loadingAreaContainer}`}>
<CircularProgress size={25} thickness={5} />
{RenderText}
</div>
);
case 'text':
return <div className={`${Styles.loadingAreaContainer}`}>{RenderText}</div>;
case 'icon':
return (
<div className={`${Styles.loadingAreaContainer}`}>
<CircularProgress size={25} thickness={5} />
</div>
);
case 'line:fix':
return <LinearProgress />;
case 'line:relative':
return <LinearProgress />;
default:
return <React.Fragment />;
}
}, [typeIcon, isHideText, text]);
return (
<>
{isLoading && (
<div
className={classNames(RenderPosition, RenderBackground, Styles.flexCentral)}
style={{ zIndex: typeZIndex }}
>
{RenderAnimation}
</div>
)}
</>
);
}
Loading.propTypes = {
typePosition: PropTypes.string,
typeBackground: PropTypes.string,
typeZIndex: PropTypes.number,
typeIcon: PropTypes.string,
isLoading: PropTypes.bool,
isHideText: PropTypes.bool,
text: PropTypes.string,
};
Loading.defaultProps = {
typePosition: 'relative',
typeBackground: '',
typeZIndex: 10000,
typeIcon: 'line:relative',
isLoading: false,
isHideText: false,
text: '',
};
export default memo(Loading);
+9
View File
@@ -0,0 +1,9 @@
export interface LoadingProps {
typePosition: 'relative' | 'absolute';
typeBackground: 'white' | 'black';
typeZIndex: number;
typeIcon: 'basic' | 'text' | 'icon' | 'line:fix' | 'line:relative';
isLoading: boolean;
isHideText?: boolean;
text?: string;
}
@@ -0,0 +1,32 @@
/* Component Style */
:local(.modalContainer) {
position: relative;
}
:local(.modalContainerTitle) {
width: 100%;
height: 40px;
position: relative;
}
:local(.modalContainerContent) {
width: 100%;
position: relative;
}
:local(.modalContainerAction) {
width: 100%;
display: flex;
justify-content: flex-end;
margin-top: 18px;
position: relative;
}
:local(.modalContainerActionStyle) {
margin-left: 16px;
}
:local(.modalContainerRemoveIcon) {
position: absolute;
right: 20px;
top: 20px;
z-index: 1000;
}
:local(.modalContainerRemoveIconStyle) {
color: #000000 !important;
}
+138
View File
@@ -0,0 +1,138 @@
import React, { memo, useMemo } from 'react';
import classNames from 'classnames';
import ClearIcon from '@material-ui/icons/Clear';
import ButtonBase from '@material-ui/core/ButtonBase';
import Dialog from '@material-ui/core/Dialog';
import Button from '@material-ui/core/Button';
import Loading from '@Components/Base/Loading';
import Styles from './index.module.css';
import { ModalProps } from './types';
function Modal(props: ModalProps): React.ReactElement {
/* Global & Local States */
const {
open,
onClose,
onConfirm,
typeSize,
typeIsLoading,
disableEscapeKeyDown,
disableBackdropClick,
disableCancelButton,
disableConfirmButton,
confirmButtonText,
cancelButtonText,
tipsText,
children,
className,
mainName,
titleClassName,
actionClassName,
title,
closeIcon = true,
disabledConfirm,
} = props;
/* Views */
const RenderSize = useMemo(() => {
switch (typeSize) {
case 'xs':
return 'xs';
case 'sm':
return 'sm';
case 'md':
return 'md';
case 'lg':
return 'lg';
case 'xl':
return 'xl';
default:
return 'sm';
}
}, [typeSize]);
const RenderCloseIcon = useMemo<React.ReactElement>(() => {
if (closeIcon) {
return (
<ButtonBase className={classNames(Styles.modalContainerRemoveIcon)} onClick={onClose}>
<ClearIcon className={classNames(Styles.modalContainerRemoveIconStyle)} fontSize="small" />
</ButtonBase>
);
}
return <></>;
}, [open, closeIcon, onClose]);
const RenderTitle = useMemo<React.ReactElement>(() => {
if (title) {
return <div className={classNames(Styles.modalContainerTitle, titleClassName)}>{title}</div>;
}
return <></>;
}, [title]);
const RenderIsLoading = useMemo<React.ReactElement>(
() => <Loading typePosition="relative" typeZIndex={10003} typeIcon="line:relative" isLoading={typeIsLoading} />,
[typeIsLoading],
);
const RenderCancelButton = useMemo<React.ReactElement>(() => {
if (!disableCancelButton) {
return (
<Button className={classNames(Styles.modalContainerActionStyle)} color="default" onClick={onClose}>
{!cancelButtonText ? '取消' : cancelButtonText}
</Button>
);
}
return <></>;
}, [disableCancelButton, cancelButtonText, onClose]);
const RenderConfirmButton = useMemo<React.ReactElement>(() => {
if (!disableConfirmButton) {
return (
<Button
className={classNames(Styles.modalContainerActionStyle)}
color="primary"
variant="contained"
onClick={onConfirm}
disabled={disabledConfirm}
>
{!confirmButtonText ? '確定' : confirmButtonText}
</Button>
);
}
return <></>;
}, [disableConfirmButton, confirmButtonText, onConfirm]);
const RenderTipsText = useMemo(() => {
if (tipsText) {
return tipsText;
}
return <></>;
}, [tipsText]);
/* Main */
return (
<Dialog
className={classNames(Styles.modalContainer, mainName)}
open={open}
onClose={onClose}
fullWidth
maxWidth={RenderSize}
disableEnforceFocus
disableEscapeKeyDown={disableEscapeKeyDown}
disableBackdropClick={disableBackdropClick}
>
<div className={classNames(className)}>
{RenderCloseIcon}
{RenderTitle}
<div className={classNames(Styles.modalContainerContent)} style={{ marginTop: title ? '18px' : '0px' }}>
{children}
</div>
<div
className={classNames(
(!disableCancelButton || !disableConfirmButton) && Styles.modalContainerAction,
actionClassName,
)}
>
{RenderTipsText}
{RenderCancelButton}
{RenderConfirmButton}
</div>
{RenderIsLoading}
</div>
</Dialog>
);
}
export default memo(Modal);
+29
View File
@@ -0,0 +1,29 @@
import React from 'react';
import { MessageSnackObject } from '@Models/Redux/Message/types';
export type ModalSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
export interface ModalProps {
open: boolean;
onClose: () => void;
onConfirm?: () => void;
typeSize?: ModalSize;
typeIsLoading?: boolean;
disableEscapeKeyDown?: boolean;
disableBackdropClick?: boolean;
disableCancelButton?: boolean;
disableConfirmButton?: boolean;
confirmButtonText?: string;
cancelButtonText?: string;
tipsText?: React.ReactNode;
children?: React.ReactNode;
className?: string;
mainName?: string;
titleClassName?: string;
actionClassName?: string;
title?: React.ReactNode;
closeIcon?: boolean;
disabledConfirm?: boolean;
blockLeave?: boolean;
blockMessage?: MessageSnackObject;
}
@@ -0,0 +1,34 @@
/* Component Style */
:local(.confirmItemContainer) {
width: 100%;
padding: 15px;
position: relative;
}
:local(.confirmItemTitle) {
width: 100%;
margin-bottom: 10px;
position: relative;
font-weight: bold !important;
display: flex;
align-items: center;
}
:local(.confirmItemSubTitle) {
width: 100%;
min-height: 23px;
margin-bottom: 15px;
position: relative;
}
:local(.confirmItemTitleStyle) {
line-height: 36px !important;
}
:local(.confirmItemActionContainer) {
width: 100%;
min-height: 23px;
display: flex;
justify-content: flex-end;
position: relative;
}
:local(.confirmItemActionButtonStyle) {
margin-left: 15px !important;
position: relative;
}
@@ -0,0 +1,98 @@
import React, { memo, useMemo } from 'react';
import classNames from 'classnames';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
import useLang from '@Hooks/useLang';
import useMessage from '@Hooks/useMessage';
import useMappedState from '@Hooks/useMappedState';
import Modal from '@Components/Base/Modal';
import { ConfirmItemObject } from './types';
import Styles from './index.module.css';
/* Confirm Item */
function ConfirmItem(props: ConfirmItemObject): React.ReactElement {
/* Global & Local States */
const { i18n } = useLang();
const { confirm, requestRemoveConfirm } = props;
/* Functions */
const onConfirmClick = (): void => {
if (confirm.onConfirm) confirm.onConfirm();
requestRemoveConfirm();
};
const onCancelClick = (): void => {
if (confirm.onCancel) confirm.onCancel();
requestRemoveConfirm();
};
/* Main */
return (
<div className={classNames(Styles.confirmItemContainer)}>
<div className={classNames(Styles.confirmItemTitle)}>
<Typography className={classNames(Styles.confirmItemTitleStyle)} variant="h2" color="textPrimary">
{confirm.typeTitle}
</Typography>
</div>
<div className={classNames(Styles.confirmItemSubTitle)}>
<Typography variant="body1" color="textPrimary" style={{ whiteSpace: 'pre-wrap' }}>
{confirm.typeContent && confirm.typeContent}
</Typography>
</div>
<div className={classNames(Styles.confirmItemActionContainer)}>
<Button
className={classNames(Styles.confirmItemActionButtonStyle)}
variant="text"
color="default"
onClick={onCancelClick}
>
{confirm.cancelText ? confirm.cancelText : i18n.t('frontend.global.operation.cancel')}
</Button>
<Button
className={classNames(Styles.confirmItemActionButtonStyle)}
variant="contained"
color="primary"
onClick={onConfirmClick}
>
{confirm.confirmText ? confirm.confirmText : i18n.t('frontend.global.operation.confirm')}
</Button>
</div>
</div>
);
}
function ModalConfirm(): React.ReactElement {
/* Global & Local States */
const reduxMessage = useMessage();
const storeMessage = useMappedState((state) => state.message);
/* Functions */
const requestRemoveConfirm = (): void => {
reduxMessage.removeConfirm();
};
/* Views */
const RenderMessages = useMemo(() => {
if (storeMessage.messageConfirmList.list.length > 0) {
return (
<ConfirmItem confirm={storeMessage.messageConfirmList.list[0]} requestRemoveConfirm={requestRemoveConfirm} />
);
}
return <React.Fragment />;
}, [storeMessage.messageConfirmList]);
const RenderIsMessageOpen = useMemo(() => {
if (storeMessage.messageConfirmList.list.length > 0) {
return true;
}
return false;
}, [storeMessage.messageConfirmList]);
/* Main */
return (
<Modal
open={RenderIsMessageOpen}
onClose={requestRemoveConfirm}
typeSize="sm"
disableCancelButton
disableConfirmButton
>
{RenderMessages}
</Modal>
);
}
export default memo(ModalConfirm);
+6
View File
@@ -0,0 +1,6 @@
import { MessageConfirmObject } from '@Models/Redux/Message/types';
export interface ConfirmItemObject {
confirm: MessageConfirmObject;
requestRemoveConfirm: () => void;
}
@@ -0,0 +1,46 @@
/* Component Style */
:local(.dialogItemContainer) {
width: 100%;
padding: 15px;
position: relative;
}
:local(.dialogItemTitle) {
width: 100%;
margin-bottom: 10px;
position: relative;
font-weight: bold !important;
display: flex;
align-items: center;
}
:local(.dialogItemSubTitle) {
width: 100%;
min-height: 23px;
margin-bottom: 15px;
position: relative;
}
:local(.dialogItemDebug) {
width: 100%;
min-height: 23px;
margin-bottom: 15px;
position: relative;
padding: 16px;
background: #e0e0e0;
}
:local(.dialogItemTitleStyle) {
line-height: 36px !important;
}
:local(.dialogItemActionContainer) {
width: 100%;
min-height: 23px;
display: flex;
justify-content: flex-end;
position: relative;
}
:local(.dialogItemDebugAction) {
left: -7px;
bottom: -2px;
position: absolute;
}
:local(.dialogItemActionButtonStyle) {
position: relative;
}
@@ -0,0 +1,103 @@
/* eslint-disable */
import React, { memo, useMemo, useState } from 'react';
import classNames from 'classnames';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
import useLang from '@Hooks/useLang';
import useMessage from '@Hooks/useMessage';
import useMappedState from '@Hooks/useMappedState';
import Modal from '@Components/Base/Modal';
import { DialogItemObject } from './types';
import Styles from './index.module.css';
/* Dialog Item */
function DialogItem(props: DialogItemObject): React.ReactElement {
/* Global & Local States */
const { i18n } = useLang();
const { dialog, requestRemoveDialog } = props;
const [hiddenDebugMessage, setHiddenDebugMessage] = useState(false);
/* Functions */
const onConfirmClick = (): void => {
if (dialog.onConfirm) dialog.onConfirm();
requestRemoveDialog();
};
/* Views */
return (
<div className={classNames(Styles.dialogItemContainer)}>
<div className={classNames(Styles.dialogItemTitle)}>
<Typography className={classNames(Styles.dialogItemTitleStyle)} variant="h2" color="textPrimary">
{dialog.typeTitle}
</Typography>
</div>
<div className={classNames(Styles.dialogItemSubTitle)}>
<Typography variant="body1" color="textPrimary" style={{ whiteSpace: 'pre-wrap' }}>
{dialog.typeContent && dialog.typeContent}
</Typography>
</div>
{dialog.typeHiddenMessage && hiddenDebugMessage && (
<div className={classNames(Styles.dialogItemDebug)}>
<Typography variant="body2" color="textSecondary" style={{ whiteSpace: 'pre-wrap' }}>
{dialog.typeHiddenMessage && dialog.typeHiddenMessage}
</Typography>
</div>
)}
<div className={classNames(Styles.dialogItemActionContainer)}>
{dialog.typeHiddenMessage && (
<Button
className={classNames(Styles.dialogItemDebugAction)}
variant="text"
color="primary"
onClick={() => setHiddenDebugMessage(!hiddenDebugMessage)}
>
{hiddenDebugMessage ? i18n.t('frontend.global.operation.debugopenclose') : i18n.t('frontend.global.operation.debugopen')}
</Button>
)}
<Button
className={classNames(Styles.dialogItemActionButtonStyle)}
variant="contained"
color="primary"
onClick={onConfirmClick}
>
{dialog.confirmText ? dialog.confirmText : i18n.t('frontend.global.operation.confirm')}
</Button>
</div>
</div>
);
}
function ModalDialog(): React.ReactElement {
/* Global & Local States */
const reduxMessage = useMessage();
const storeMessage = useMappedState(state => state.message);
/* Functions */
const requestRemoveDialog = (): void => {
reduxMessage.removeDialog();
};
/* Views */
const RenderMessages = useMemo(() => {
if (storeMessage.messageDialogList.list.length > 0) {
return <DialogItem dialog={storeMessage.messageDialogList.list[0]} requestRemoveDialog={requestRemoveDialog} />;
}
return <React.Fragment />;
}, [storeMessage.messageDialogList]);
const RenderIsMessageOpen = useMemo(() => {
if (storeMessage.messageDialogList.list.length > 0) {
return true;
}
return false;
}, [storeMessage.messageDialogList]);
/* Main */
return (
<Modal
open={RenderIsMessageOpen}
onClose={requestRemoveDialog}
typeSize="sm"
disableCancelButton
disableConfirmButton
>
{RenderMessages}
</Modal>
);
}
export default memo(ModalDialog);
+6
View File
@@ -0,0 +1,6 @@
import { MessageDialogObject } from '@Models/Redux/Message/types';
export interface DialogItemObject {
dialog: MessageDialogObject;
requestRemoveDialog: () => void;
}
+11
View File
@@ -0,0 +1,11 @@
import React, { memo } from 'react';
import useMappedRoute from '@Hooks/useMappedRoute';
function Routes(): React.ReactElement {
/* Global & Local State */
const routeDom = useMappedRoute([], 'home', 'login');
/* Main */
return <>{routeDom}</>;
}
export default memo(Routes);
@@ -0,0 +1,10 @@
:local(.appCheckViewContainer) {
width: 100%;
height: 100%;
position: fixed;
top: 0px;
left: 0px;
display: flex;
justify-content: center;
align-items: center;
}
@@ -0,0 +1,23 @@
import React, { memo } from 'react';
import classNames from 'classnames';
import useDidMount from '@Hooks/useDidMount';
import useReduxApi from '@Hooks/useReduxApi';
import GifLoader from '@Components/Base/GifLoader';
import Styles from './index.module.css';
function AppCheckView(): React.ReactElement {
/* Global & Local States */
const reduxUser = useReduxApi('user');
/* Functions */
const initialize = (): void => {
reduxUser('getUserIsLogin', []);
};
/* Hooks */
useDidMount(() => {
initialize();
});
/* Main */
return <div className={classNames(Styles.appCheckViewContainer)}><GifLoader /></div>;
}
export default memo(AppCheckView);
@@ -0,0 +1,39 @@
/* Component Style */
:local(.appContainer) {
width: 100%;
height: 100%;
position: relative;
background: #f4f4f4;
}
:local(.appHeader) {
width: 100% !important;
height: 60px !important;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
background: #ffffff;
position: relative;
z-index: 1300;
}
:local(.appBody) {
width: 100% !important;
height: calc(100% - 60px) !important;
display: flex;
}
:local(.appHeart) {
width: calc(100% - 230px);
display: flex;
flex-wrap: wrap;
flex: 1;
position: relative;
}
:local(.appSide) {
max-width: 230px;
position: relative;
}
:local(.appMain) {
flex: 1 1;
height: 100%;
position: relative;
overflow-x: hidden;
overflow-y: hidden;
}
@@ -0,0 +1,17 @@
import React, { memo } from 'react';
import classNames from 'classnames';
import Routes from '@Components/Common/Routes';
import Styles from './index.module.css';
function MainAppView(): React.ReactElement {
/* Global & Local State */
/* Data */
/* Main */
return (
<div className={classNames(Styles.appContainer)}>
<Routes />
</div>
);
}
export default memo(MainAppView);
@@ -0,0 +1,6 @@
/* Component Style */
:local(.homeContainer) {
width: 100%;
height: 100%;
position: relative;
}
+10
View File
@@ -0,0 +1,10 @@
import React, { memo } from 'react';
import classNames from 'classnames';
import Styles from './index.module.css';
function Home(): React.ReactElement {
/* Global & Local State */
/* Main */
return <div className={classNames(Styles.homeContainer)}>Home</div>;
}
export default memo(Home);
@@ -0,0 +1,10 @@
/* Component Style */
:local(.oAuthContainer) {
width: 100%;
height: 100%;
position: fixed;
top: 0px;
left: 0px;
background: white;
z-index: 100;
}
+80
View File
@@ -0,0 +1,80 @@
import React, { memo } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { Base64 } from 'js-base64';
import qs from 'qs';
import classNames from 'classnames';
import useLang from '@Hooks/useLang';
import useMessage from '@Hooks/useMessage';
import usaDidMount from '@Hooks/useDidMount';
import useReduxApi from '@Hooks/useReduxApi';
import Loading from '@Components/Base/Loading';
import { UserOAuthResponse, OAuthResult, APIError } from './types';
import Styles from './index.module.css';
function OAuth(): React.ReactElement {
/* Global & Local State */
const { i18n } = useLang();
const reduxMessage = useMessage();
const reduxUser = useReduxApi('user');
const routeHistory = useHistory();
const routeLocation = useLocation();
/* Functions */
const onOAuthSuccess = (token: string): void => {
reduxUser('postUserTokenInfoSignIn', [token]);
};
const onOAuthFailed = (error: APIError): void => {
reduxMessage.error(error);
routeHistory.push('/');
};
const parseQuery = (base64String: string): OAuthResult => {
const result = Base64.decode(base64String);
const json: OAuthResult = JSON.parse(result);
return json;
};
const validateByType = (query: UserOAuthResponse): void => {
if (Object.keys(query).length > 0) {
if (query.error) {
const parseResult = parseQuery(query.error);
const error: APIError = {
code: parseResult.code,
message: parseResult.message,
errorStack: parseResult.errorStack,
errorMessage: parseResult.errorMessage,
};
onOAuthFailed(error);
return;
}
if (query.success) {
const parseResult = parseQuery(query.success);
onOAuthSuccess(parseResult.token);
}
}
};
const initialize = async (): Promise<void> => {
const resultQuery: UserOAuthResponse = qs.parse(routeLocation.search, {
ignoreQueryPrefix: true,
});
if (Object.keys(resultQuery).length > 0) {
validateByType(resultQuery);
} else {
routeHistory.push('/');
}
};
/* Hooks */
usaDidMount(() => {
initialize();
});
/* Main */
return (
<div className={classNames(Styles.oAuthContainer)}>
<Loading
typeIcon="basic"
typePosition="absolute"
typeBackground="white"
isLoading
text={i18n.t('frontend.global.property.validate')}
/>
</div>
);
}
export default memo(OAuth);
+11
View File
@@ -0,0 +1,11 @@
import { APIError } from '@Models/GeneralTypes';
import { UserOAuthResponse, UserTokenInfo } from '@Models/Redux/User/types';
export interface OAuthResult extends UserTokenInfo, APIError {
code: number;
message: string;
errorStack: string;
errorMessage: string;
}
export { UserOAuthResponse, UserTokenInfo, APIError };
@@ -0,0 +1,77 @@
/* Component Style */
:local(.signInContainer) {
width: 100%;
height: 100%;
position: relative;
}
:local(.signInPanelContainer) {
padding: 0px calc(100% / 12) !important;
position: relative;
}
:local(.signInTitleContainer) {
width: 100%;
min-height: 100px;
padding: 40px 0px 0px 0px;
position: relative;
font-weight: bold !important;
}
:local(.signInFormContainer) {
width: 100%;
display: flex;
position: relative;
}
:local(.signInActionContainer) {
width: 100%;
position: relative;
}
:local(.signInTipsContainer) {
width: 100%;
min-height: 50px;
margin-bottom: 20px;
display: flex;
flex-wrap: wrap;
position: relative;
}
:local(.signInLanguageContainer) {
width: 100%;
min-height: 50px;
display: flex;
flex-wrap: wrap;
align-items: center;
position: relative;
}
:local(.signInFormMargin) {
width: 100%;
margin-bottom: 12px !important;
position: relative;
}
:local(.signInActionButtonStyle) {
min-width: 182px !important;
margin-right: 23px !important;
}
:local(.signInActionOrStyle) {
margin-bottom: 24px !important;
}
:local(.signInLanguageStyle) {
margin-left: 10px !important;
}
:local(.signInTitleMainStyle) {
margin-bottom: 10px;
}
:local(.signInTitleSubStyle) {
margin-bottom: 10px;
}
/* Responsive Style */
@media (max-width: 767px) {
:local(.signInActionButtonStyle) {
width: 100% !important;
min-width: 0px !important;
margin-bottom: 23px !important;
}
:local(.signInActionButtonGoogleStyle) {
width: 100% !important;
min-width: 0px !important;
margin-bottom: 23px !important;
}
}
+68
View File
@@ -0,0 +1,68 @@
import React, { memo, useState, useEffect } from 'react';
import classNames from 'classnames';
import useLang from '@Hooks/useLang';
import useDidMount from '@Hooks/useDidMount';
import useReduxApi from '@Hooks/useReduxApi';
import useMappedState from '@Hooks/useMappedState';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
import Loading from '@Components/Base/Loading';
import BASE_URL from '@Base/url';
import Styles from './index.module.css';
const GUEST_URL = BASE_URL.GUEST;
function SignIn(): React.ReactElement {
/* Global & Local State */
const { i18n } = useLang();
const reduxUser = useReduxApi('user');
const storeUser = useMappedState((state) => state.user);
const storeGlobal = useMappedState((state) => state.global);
const [isFirstInitial, setIsFirstInitial] = useState(false);
const [isLoading, setIsLoading] = useState(false);
/* Functions */
const onSubmitSSO = (): void => {
const backUrl = `${window.location.origin}${GUEST_URL.BASE_PAGE_OAUTH}`;
setIsLoading(true);
reduxUser('getUserSSO', [backUrl]);
};
const initialize = (): void => {
if (storeGlobal.globalEnv.env.EnvName === 'prod') {
onSubmitSSO();
}
setIsFirstInitial(true);
};
/* Hooks */
useDidMount(() => {
initialize();
});
useEffect(() => {
if (!isFirstInitial) return;
setIsLoading(false);
}, [storeUser.userAccount]);
/* Main */
return (
<div className={classNames(Styles.signInContainer)}>
<div className={classNames(Styles.signInPanelContainer)}>
<div className={classNames(Styles.signInTitleContainer)}>
<Typography className={classNames(Styles.signInTitleMainStyle)} variant="h4" color="textPrimary">
{i18n.t('frontend.local.signin.logintitle')}
</Typography>
</div>
<div className={classNames(Styles.signInActionContainer)}>
<Button
className={classNames(Styles.signInActionButtonStyle)}
variant="contained"
color="primary"
onClick={onSubmitSSO}
>
{i18n.t('frontend.global.operation.sso')}
</Button>
</div>
</div>
<Loading typePosition="absolute" typeZIndex={20000} typeIcon="line:fix" isLoading={isLoading} />
</div>
);
}
export default memo(SignIn);
+37
View File
@@ -0,0 +1,37 @@
*,
:after,
:before {
box-sizing: border-box;
}
html {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
box-sizing: border-box;
}
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
--ck-z-default: 100;
--ck-z-modal: calc(var(--ck-z-default) + 999);
}
#root {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
td:first-child {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
td:last-child {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
+12
View File
@@ -0,0 +1,12 @@
import { EnvConfig } from '../types';
const config: EnvConfig = {
EnvName: 'default',
EnvShortName: 'l',
EnvUrl: '',
APIUrl: 'http://localhost:10230',
I18nLocalName: 'default-lang',
JwtTokenLocalName: 'default-token',
};
export default config;
+12
View File
@@ -0,0 +1,12 @@
import { EnvConfig } from '../types';
const config: EnvConfig = {
EnvName: 'development',
EnvShortName: 'd',
EnvUrl: '',
APIUrl: 'http://localhost:10230',
I18nLocalName: 'development-lang',
JwtTokenLocalName: 'development-token',
};
export default config;
+57
View File
@@ -0,0 +1,57 @@
/* eslint-disable no-console */
import Default from './Config/Default';
import Development from './Config/Development';
import { EnvConfig } from './types';
class Config {
constructor() {
this.initialize();
}
public env: EnvConfig = Default;
public replaceCompanyLogo = false;
initialize(): void {
if (process.env.APP_ENV) {
this.setEnv(process.env.APP_ENV);
}
}
setEnv(env: string): void {
switch (env) {
case 'dev':
this.env = Development;
break;
default:
this.env = Default;
break;
}
}
getEnv(): EnvConfig {
return this.env;
}
get HostApiUrl(): string {
return `${this.env.APIUrl}/api`;
}
get HostUrl(): string {
return this.env.EnvUrl;
}
get TokenLocalStorageName(): string {
return this.env.JwtTokenLocalName;
}
get I18nLocalStorageName(): string {
return this.env.I18nLocalName;
}
get EnvName(): string {
return this.env.EnvName;
}
}
export default Config;
+8
View File
@@ -0,0 +1,8 @@
export interface EnvConfig {
readonly EnvName: string;
readonly EnvShortName: string;
readonly EnvUrl: string;
readonly APIUrl: string;
readonly I18nLocalName: string;
readonly JwtTokenLocalName: string;
}
+5
View File
@@ -0,0 +1,5 @@
import { useEffect, EffectCallback } from 'react';
export default function useDidMount(effect: EffectCallback): void {
return useEffect(effect, []);
}
+9
View File
@@ -0,0 +1,9 @@
import { useDispatch as BaseUseDispatch } from 'react-redux';
import { Action, AnyAction } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { StoreState } from '@Reducers/_InitializeStore/types';
export type UseDispatch<T extends Action = AnyAction> = () => ThunkDispatch<StoreState, Record<string, unknown>, T>;
const useDispatch: UseDispatch = BaseUseDispatch;
export default useDispatch;
+16
View File
@@ -0,0 +1,16 @@
import { useMemo } from 'react';
import I18n from '@Models/Core/I18n';
import useMappedState from './useMappedState';
export interface UseLang {
i18n: I18n;
}
/**
* :  Class Instance
*/
export default function useLang(): UseLang {
const storeGlobal = useMappedState((state) => state.global);
return useMemo(() => ({ i18n: storeGlobal.globalI18n }), [storeGlobal.globalLang]);
}
+112
View File
@@ -0,0 +1,112 @@
import React, { useMemo } from 'react';
import {
Route, Redirect, Switch, useRouteMatch,
} from 'react-router-dom';
import LazyComponent from '@Components/Base/Lazy';
import BaseRoutes from '@Base/routes';
import { RouteItem } from '@Base/routes/types';
import useMappedState from './useMappedState';
type useMappedRouteProps = React.ReactNode;
/**
* :  Route SubRoute
*/
export default function useMappedRoute(
routeLayer: string[],
signInRedirect = '',
noSignInRedirect = '',
): useMappedRouteProps {
/* Global & Local State */
const routeMatch = useRouteMatch();
const storeUser = useMappedState((state) => state.user);
/* Data */
const ReturnRoutes: RouteItem[] = useMemo(() => {
let fullRoutes: RouteItem[] = [];
let result: RouteItem[] | null = null;
let hadResult = false;
if (storeUser.userIsLogin) {
Object.values(BaseRoutes.Admin).forEach((route) => {
fullRoutes = fullRoutes.concat(route);
});
} else {
Object.values(BaseRoutes.Guest).forEach((route) => {
fullRoutes = fullRoutes.concat(route);
});
}
if (routeLayer.length === 0) {
result = fullRoutes;
}
for (let i = 0; i < routeLayer.length; i += 1) {
if (!hadResult) {
const recentRouteObject = fullRoutes.find((route) => route.name === routeLayer[i]);
if (recentRouteObject) {
result = recentRouteObject.subRoute;
hadResult = true;
}
}
if (hadResult && result) {
const recentRouteObject = result.find((route) => route.name === routeLayer[i]);
if (recentRouteObject) {
result = recentRouteObject.subRoute;
}
}
}
if (result) {
return result;
}
return [];
}, [storeUser.userIsLogin]);
const ReturnReDirect = useMemo<string>(() => {
if (storeUser.userIsLogin) {
if (ReturnRoutes.length > 0) {
return ReturnRoutes[0].route;
}
}
if (ReturnRoutes.length > 0) {
return ReturnRoutes[0].route;
}
return '/';
}, [storeUser.userIsLogin, signInRedirect, noSignInRedirect]);
/* View */
const RenderRoutes = useMemo(
() => (
<Switch>
<Route
exact
path={`${routeMatch.url.replace('/', '') === '' ? '/' : `/${routeMatch.url.replace('/', '')}`}`}
render={() => (
<Redirect
to={`${
routeMatch.url.replace('/', '') === '' ? '' : `/${routeMatch.url.replace('/', '')}`
}${ReturnReDirect}`}
/>
)}
/>
{ReturnRoutes.map((route) => (
<Route
key={route.route}
exact={route.exact}
path={`${routeMatch.path.replace('/', '') === '' ? '' : `/${routeMatch.path.replace('/', '')}`}${
route.route
}`}
render={() => (
<LazyComponent
componentImport={route.component}
componentChunkName={`${route.name}Chunk`}
componentProps={{}}
/>
)}
/>
))}
<Redirect
to={`${routeMatch.url.replace('/', '') === '' ? '' : `/${routeMatch.url.replace('/', '')}`}${ReturnReDirect}`}
/>
</Switch>
),
[storeUser.userIsLogin, routeMatch, ReturnReDirect],
);
/* Main */
return <>{RenderRoutes}</>;
}
+13
View File
@@ -0,0 +1,13 @@
import { useSelector } from 'react-redux';
import { StoreState } from '@Reducers/_InitializeStore/types';
/**
* :  Redux
*/
export default function useMappedState<T>(
mapState: (state: StoreState) => T,
equalityFn?: (left: T, right: T) => boolean,
): T {
return useSelector<StoreState, T>(mapState, equalityFn);
}
+55
View File
@@ -0,0 +1,55 @@
import { useMemo } from 'react';
import { APIError } from '@Models/GeneralTypes';
import {
postMessageDialog,
postMessageConfirm,
removeMessageDialog,
removeMessageConfirm,
} from '@Reducers/message/actions';
import { MessageDialogObject, MessageConfirmObject } from '@Models/Redux/Message/types';
import { errorCatch } from '@Reducers/_Capture/errorCapture';
import useDispatch from './useDispatch';
interface useMessageDefine {
dialog: (dialog: MessageDialogObject) => void;
confirm: (confirm: MessageConfirmObject) => void;
removeDialog: () => void;
removeConfirm: () => void;
error: (error: APIError) => void;
}
/**
* :  Redux Action Message Action
*/
export default function useMessage(): useMessageDefine {
/* Global & Local State */
const dispatch = useDispatch();
/* Functions */
const apiCallPostMessageDialog = (dialog: MessageDialogObject): void => {
dispatch(postMessageDialog(dialog));
};
const apiCallPostMessageConfirm = (confirm: MessageConfirmObject): void => {
dispatch(postMessageConfirm(confirm));
};
const apiCallRemoveMessageDialog = (): void => {
dispatch(removeMessageDialog());
};
const apiCallRemoveMessageConfirm = (): void => {
dispatch(removeMessageConfirm());
};
const apiCallError = (error): void => {
dispatch(errorCatch(error));
};
/* Main */
return useMemo(
() => ({
dialog: apiCallPostMessageDialog,
confirm: apiCallPostMessageConfirm,
removeDialog: apiCallRemoveMessageDialog,
removeConfirm: apiCallRemoveMessageConfirm,
error: apiCallError,
}),
[],
);
}
+65
View File
@@ -0,0 +1,65 @@
/* eslint-disable no-console */
import { useCallback } from 'react';
import { ThunkAction } from 'redux-thunk';
import { StoreState } from '@Reducers/_InitializeStore/types';
import useDispatch from './useDispatch';
type useReduxAPICallBack = (action: string, props: Array<unknown>) => void;
type actionType =
| 'user'
| 'global'
| 'message';
interface ReduxAction {
[key: string]: (...props: Array<unknown>) => ThunkAction<Promise<void>, StoreState, unknown, { type: string }>;
}
interface ReduxActionModule {
[key: string]: ReduxAction;
}
const VALID_ACTIONS: actionType[] = [
'user',
'global',
'message',
];
const LOAD_MODULE: ReduxActionModule = {};
/**
* :  Redux Action
*/
export default function useReduxAPI(actionType: actionType): useReduxAPICallBack {
/* Global & Local State */
const dispatch = useDispatch();
/* Main */
return useCallback(
(action: string, props: Array<unknown>) => {
const isInclude = VALID_ACTIONS.includes(actionType);
let reduxType: ReduxAction = {};
if (isInclude) {
if (LOAD_MODULE[actionType]) {
reduxType = LOAD_MODULE[actionType];
} else {
LOAD_MODULE[actionType] = require(`@Reducers/${actionType}/actions`);
reduxType = LOAD_MODULE[actionType];
}
if (reduxType) {
const reduxAction = reduxType[action];
if (reduxAction) {
dispatch(reduxAction(...props));
} else {
console.warn(`Can't find the action ::: ${action}`);
}
} else {
console.warn(`Can't load module ::: ${reduxType}`);
}
} else {
console.warn(`Can't find the action type ::: ${actionType}`);
}
},
[actionType],
);
}
+19
View File
@@ -0,0 +1,19 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import '@CSS/index.css';
import '@Plugin/index';
async function renderMainApp(): Promise<void> {
const mainAppProvider = (await import('@Providers/MainAppProvider')).default;
const Provider = mainAppProvider({
Router: BrowserRouter,
appKey: 520,
routerProps: {
basename: '',
},
});
ReactDOM.render(<Provider />, document.getElementById('root'));
}
renderMainApp();
+3
View File
@@ -0,0 +1,3 @@
const lang = {};
export default lang;
+23
View File
@@ -0,0 +1,23 @@
const lang = {
'frontend.global.operation.confirm': '確認',
'frontend.global.operation.ok': '確定',
'frontend.global.operation.cancel': '取消',
'frontend.global.operation.signout': '登出',
'frontend.global.operation.sso': '單一登入',
'frontend.global.property.routehome': '首頁',
'frontend.local.signin.logintitle': 'KeyCloak Demo Server',
'frontend.global.error.9999': '未知的錯誤',
'frontend.global.error.0000': '錯誤',
'frontend.global.error.1001': '建立',
'frontend.global.error.1002': '已接收',
'frontend.global.error.1003': '資料格式錯誤',
'frontend.global.error.1004': '尚未登入',
'frontend.global.error.1005': '無權限可瀏覽',
'frontend.global.error.1006': '查無此資料',
'frontend.global.error.1007': '伺服器內部錯誤,請尋求客服的協助',
};
export default lang;
+1
View File
@@ -0,0 +1 @@
export default ['zh-TW', 'en'];
+75
View File
@@ -0,0 +1,75 @@
import Immerable from '@Models/GeneralImmer';
import tw from '@Langs/data/tw';
import en from '@Langs/data/en';
import { LangObject, LangList } from './types';
const LANGUAGE_BASE_LIST = [
{
lang: 'frontend.global.language.en',
value: 'en',
},
{
lang: 'frontend.global.language.tw',
value: 'tw',
},
];
class I18n extends Immerable {
public lang: string;
public langDefault: LangObject;
public langList: LangList;
public langBase: LangObject;
constructor(lang: string) {
super();
this.lang = lang;
this.langDefault = en;
this.langList = LANGUAGE_BASE_LIST;
this.langBase = en;
this.switchLanguage(this.lang);
}
t(key: string): string {
let text = this.langBase[key];
if (!text) {
text = this.langDefault[key] || '';
}
return text;
}
switchLanguage(language: string): void {
switch (language) {
case 'en':
this.langBase = en;
break;
case 'tw':
this.langBase = tw;
break;
default:
this.langBase = en;
break;
}
}
getSystemLang = (): string => {
const lang = navigator.language.toLowerCase();
switch (lang) {
case 'en':
return 'en';
case 'tw':
case 'zh-tw':
return 'tw';
default:
return 'en';
}
};
get languages(): LangList {
return this.langList;
}
}
export default I18n;
+10
View File
@@ -0,0 +1,10 @@
export interface LangObject {
[key: string]: string;
}
export interface LangListItem {
lang: string;
value: string;
}
export type LangList = LangListItem[];
+5
View File
@@ -0,0 +1,5 @@
import { immerable } from 'immer';
export default class Immerable {
protected [immerable] = true;
}
+40
View File
@@ -0,0 +1,40 @@
export interface Pager {
page: number;
count: number;
total: number;
}
export interface TimeStamp {
createdAt: string;
updatedAt: string;
}
export interface Range {
max: number;
min: number;
}
export interface Condition {
page: number;
}
export interface Attachment {
id: string;
name: string;
file?: File;
old?: boolean;
}
export interface AttachmentInDB {
path: string;
filename: string;
url: string;
file?: File;
old?: boolean;
}
export interface ValidFileFormat {
type: string;
display: string;
}
export interface APIError {
message: string;
code: number;
errorStack: string;
errorMessage: string;
}
export type Status = '建立通知' | '不通知' | '重複' | '不適用' | '未查看' | '已查看';
+53
View File
@@ -0,0 +1,53 @@
import Immerable from '@Models/GeneralImmer';
import I18n from '@Models/Core/I18n';
import { MiddleWareObject, MiddleWareAPI, MiddleWareEnv } from './types';
class Global extends Immerable {
public globalLoading: boolean;
public globalLang: string;
public globalSideBar: boolean;
public globalSideBarStatic: boolean;
public globalAPI: MiddleWareAPI;
public globalEnv: MiddleWareEnv;
public globalI18n: I18n;
public constructor(middleware: MiddleWareObject) {
super();
this.globalLoading = false;
this.globalLang = 'tw';
this.globalSideBar = false;
this.globalSideBarStatic = true;
this.globalAPI = middleware.api;
this.globalEnv = middleware.env;
this.globalI18n = new I18n(this.globalLang);
}
public initialize(): void {
this.globalLoading = false;
}
public updateGlobalLoading(newGlobalLoading: boolean): void {
this.globalLoading = newGlobalLoading;
}
public updateGlobalLang(newGlobalLang: string): void {
this.globalLang = newGlobalLang;
this.globalI18n.switchLanguage(newGlobalLang);
}
public updateGlobalSideBar(newGlobalSideBarState: boolean): void {
this.globalSideBar = newGlobalSideBarState;
}
public updateGlobalSideBarStatic(newGlobalSideBarStatic: boolean): void {
this.globalSideBarStatic = newGlobalSideBarStatic;
}
}
export default Global;
+10
View File
@@ -0,0 +1,10 @@
import API from '@API/index';
import Env from '@Env/index';
import { MiddleWare } from '@Reducers/_initializeMiddleware/types';
export type MiddleWareObject = MiddleWare;
export type MiddleWareAPI = API;
export type MiddleWareEnv = Env;
+61
View File
@@ -0,0 +1,61 @@
import Immerable from '@Models/GeneralImmer';
import cloneDeep from 'lodash/cloneDeep';
import {
MessageDialogObject,
MessageDialogList,
MessageConfirmObject,
MessageConfirmList,
} from './types';
class Message extends Immerable {
public messageDialogList: MessageDialogList;
public messageConfirmList: MessageConfirmList;
public constructor() {
super();
this.messageDialogList = {
list: [],
};
this.messageConfirmList = {
list: [],
};
}
public initialize(): void {
this.messageDialogList = {
list: [],
};
this.messageConfirmList = {
list: [],
};
}
public updateMessageDialogList(newMessageDialogObject: MessageDialogObject): void {
const newCloneMessageDialogObject = cloneDeep(newMessageDialogObject);
const newCloneMessageDialogList = cloneDeep(this.messageDialogList);
newCloneMessageDialogList.list.push(newCloneMessageDialogObject);
this.messageDialogList = newCloneMessageDialogList;
}
public removeMessageDialog(): void {
const newCloneMessageDialogList = cloneDeep(this.messageDialogList.list);
const newSliceDialogList = newCloneMessageDialogList.slice(1);
this.messageDialogList.list = newSliceDialogList;
}
public updateMessageConfirmList(newMessageConfirmObject: MessageConfirmObject): void {
const newCloneMessageConfirmObject = cloneDeep(newMessageConfirmObject);
const newCloneMessageConfirmList = cloneDeep(this.messageConfirmList);
newCloneMessageConfirmList.list.push(newCloneMessageConfirmObject);
this.messageConfirmList = newCloneMessageConfirmList;
}
public removeMessageConfirm(): void {
const newCloneMessageConfirmList = cloneDeep(this.messageConfirmList.list);
const newSliceConfirmList = newCloneMessageConfirmList.slice(1);
this.messageConfirmList.list = newSliceConfirmList;
}
}
export default Message;
+28
View File
@@ -0,0 +1,28 @@
import React from 'react';
export interface MessageDialogObject {
typeMessage: 'success' | 'warning' | 'info' | 'danger';
typeTitle: string;
typeContent?: string;
typeHiddenMessage?: string;
confirmText?: string;
onConfirm?: () => void;
}
export interface MessageDialogList {
list: MessageDialogObject[];
}
export interface MessageConfirmObject {
typeMessage: 'success' | 'warning' | 'info' | 'danger';
typeTitle: string;
typeContent: string | React.ReactElement;
confirmText?: string;
cancelText?: string;
onConfirm?: () => void;
onCancel?: () => void;
}
export interface MessageConfirmList {
list: MessageConfirmObject[];
}
+76
View File
@@ -0,0 +1,76 @@
import Immerable from '@Models/GeneralImmer';
import cloneDeep from 'lodash/cloneDeep';
import { UserSignInInfo, UserAccountInfo, UserOAuthUrl } from './types';
class User extends Immerable {
public userIsLogin: boolean;
public userAuthCheck: boolean;
public userAccount: UserSignInInfo;
public userToken: string;
public userOAuth: UserOAuthUrl;
public constructor() {
super();
this.userIsLogin = false;
this.userAuthCheck = true;
this.userAccount = {
account: {
_id: '',
username: '',
email: '',
display_name: '',
createdAt: '',
updatedAt: '',
},
};
this.userToken = '';
this.userOAuth = {
url: '',
};
}
public initialize(): void {
this.userIsLogin = false;
this.userAuthCheck = false;
this.userAccount = {
account: {
_id: '',
username: '',
email: '',
display_name: '',
createdAt: '',
updatedAt: '',
},
};
this.userToken = '';
this.userOAuth = {
url: '',
};
}
public updateUserLoginState(newUserLoginState: boolean): void {
this.userIsLogin = newUserLoginState;
}
public updateUserAuthCheck(newUserAuthCheck: boolean): void {
this.userAuthCheck = newUserAuthCheck;
}
public updateUserToken(newUserToken: string): void {
this.userToken = cloneDeep(newUserToken);
}
public updateUserAccountInFo(newUserAccount: UserAccountInfo): void {
this.userAccount.account = cloneDeep(newUserAccount);
}
public updateUserOAuthUrl(newUserOAuth: UserOAuthUrl): void {
this.userOAuth = cloneDeep(newUserOAuth);
}
}
export default User;
+21
View File
@@ -0,0 +1,21 @@
import { TimeStamp } from '@Models/GeneralTypes';
export interface UserAccountInfo extends TimeStamp {
_id: string;
username: string;
email: string;
display_name: string;
}
export interface UserSignInInfo {
account: UserAccountInfo;
}
export interface UserTokenInfo {
token: string;
}
export interface UserOAuthUrl {
url: string;
}
export interface UserOAuthResponse {
success?: string;
error?: string;
}
+8
View File
@@ -0,0 +1,8 @@
import 'react-app-polyfill/ie11';
import 'react-app-polyfill/stable';
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
dayjs.extend(timezone); // Support Time Zone
dayjs.extend(utc); // Support UTC Time
+26
View File
@@ -0,0 +1,26 @@
import React from 'react';
import { Provider } from 'react-redux';
import createAppStore from '@Reducers/_InitializeStore/createAppStore';
import createAppModel from '@Reducers/_InitializeModel/createAppModel';
import createMiddleware from '@Reducers/_initializeMiddleware/createMiddleware';
import { AppProps } from '@Components/App/types';
import App from '@Components/App';
export interface CreateProviderParams {
Router: AppProps['Router'];
routerProps?: AppProps['routerProps'];
appKey: number;
}
export default function MainAppProvider(params: CreateProviderParams): React.FC {
const { Router, appKey, routerProps } = params;
const initialMiddleWare = createMiddleware();
const initialState = createAppModel(initialMiddleWare);
const initialStore = createAppStore({ initialState, initialMiddleWare });
return (): React.ReactElement => (
<Provider store={initialStore}>
<App key={appKey} Router={Router} routerProps={routerProps} />
</Provider>
);
}
+69
View File
@@ -0,0 +1,69 @@
/// <reference types="node" />
/// <reference types="react" />
/// <reference types="react-dom" />
declare namespace NodeJS {
interface ProcessEnv {
readonly NODE_ENV: 'development' | 'production' | 'test';
readonly PUBLIC_URL: string;
}
}
declare module '*.avif' {
const src: string;
export default src;
}
declare module '*.bmp' {
const src: string;
export default src;
}
declare module '*.gif' {
const src: string;
export default src;
}
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.jpeg' {
const src: string;
export default src;
}
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.webp' {
const src: string;
export default src;
}
declare module '*.svg' {
import * as React from 'react';
export const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement> & { title?: string }>;
const src: string;
export default src;
}
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.module.scss' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.module.sass' {
const classes: { readonly [key: string]: string };
export default classes;
}
+52
View File
@@ -0,0 +1,52 @@
import { ThunkAction } from 'redux-thunk';
import { StoreState } from '@Reducers/_InitializeStore/types';
import { postMessageDialog } from '@Reducers/message/actions';
import { MessageDialogObject } from '@Models/Redux/Message/types';
import I18n from '@Models/Core/I18n';
import { APIError } from './types';
export const errorCatch = (
error: APIError,
actions?: () => void,
): ThunkAction<Promise<void>, StoreState, unknown, { type: string }> => async (dispatch, getState): Promise<void> => {
const storeGlobal = getState().global;
const env = storeGlobal.globalEnv;
const langs = storeGlobal.globalLang;
const i18n = new I18n(langs);
let messageHidden = '';
let messageTitle = i18n.t('frontend.global.error.0000');
let messageContent = '';
if (error && typeof error === 'object') {
if ('code' in error) {
if (i18n.t(`frontend.global.error.${error.code}`)) {
messageTitle = `${messageTitle}: (${error.code})`;
messageContent = i18n.t(`frontend.global.error.${error.code}`);
}
if (!i18n.t(`frontend.global.error.${error.code}`) && 'message' in error) {
messageContent = error.message;
}
if (env.EnvName !== 'production' && 'errorStack' in error) {
messageHidden += `\n${error.errorStack}`;
}
}
} else if (error && typeof error === 'string') {
messageTitle = i18n.t('frontend.global.error.0000');
messageContent = error;
} else {
messageContent = i18n.t('frontend.global.error.9999');
}
const data: MessageDialogObject = {
typeMessage: 'danger',
typeTitle: messageTitle,
typeContent: messageContent,
onConfirm: () => {
if (actions) {
actions();
}
},
};
if (messageHidden) {
data.typeHiddenMessage = messageHidden;
}
dispatch(postMessageDialog(data));
};
+91
View File
@@ -0,0 +1,91 @@
import API from '@API/index';
import Env from '@Env/index';
import { postGlobalReduxReset } from '@Reducers/global/actions';
import { errorCatch } from './errorCapture';
const EXPIRE_TIME_MS = 1000 * 60 * 60 * 24; // 確認一天前有沒有過期
/* General Action : For Local Token Storage */
export const removeLocalUserToken = (env: Env): void => {
const localTokenName = env.TokenLocalStorageName;
const localToken = localStorage.getItem(localTokenName);
if (localToken) {
localStorage.removeItem(localTokenName);
}
};
export const saveLocalUserToken = (env: Env, token: string): void => {
const localTokenName = env.TokenLocalStorageName;
const localToken = localStorage.getItem(localTokenName);
if (localToken) {
removeLocalUserToken(env);
}
localStorage.setItem(localTokenName, JSON.stringify({ token }));
};
export const readLocalUserToken = async (env: Env, api: API): Promise<boolean> => {
const localTokenName = env.TokenLocalStorageName;
const localToken = localStorage.getItem(localTokenName);
if (localToken) {
const localTokenJson = JSON.parse(localToken);
api.updateAccessToken(localTokenJson.token);
const isAccessExpired = await api.checkAccessTokenExpired(EXPIRE_TIME_MS);
if (isAccessExpired) {
removeLocalUserToken(env);
return false;
}
const result = await api.user.postUserRefreshToken();
if (!result.error) {
api.updateAccessToken(result.token);
localStorage.setItem(localTokenName, JSON.stringify({ token: result.token }));
return true;
}
if (result.error) {
api.removeAccessToken();
return false;
}
}
return false;
};
export const checkUserTokenIsValid = async (env: Env, api: API, dispatch: any): Promise<boolean> => {
const isAccessTokenValid = await api.checkAccessTokenValid(); // 確認 Token 是否合法
/* 合法 */
if (isAccessTokenValid) {
const isAccessTokenExpired = await api.checkAccessTokenExpired(EXPIRE_TIME_MS); // 確認 Token 是否過期
/* 沒過期 */
if (!isAccessTokenExpired) {
return true;
}
/* 過期 */
if (isAccessTokenExpired) {
const result = await api.user.postUserRefreshToken(); // 嘗試更新 Token
/* 更新成功, 更換 API Token */
if (!result.error) {
const localTokenName = env.TokenLocalStorageName;
const localToken = localStorage.getItem(localTokenName);
if (localToken) {
api.removeAccessToken();
localStorage.removeItem(localTokenName);
}
localStorage.setItem(localTokenName, JSON.stringify({ token: result.token }));
api.updateAccessToken(result.token);
return true;
}
/* 更新失敗, 清空所有狀態, 讓使用者重新登入 */
if (result.error) {
api.removeAccessToken();
dispatch(postGlobalReduxReset());
dispatch(errorCatch(result.error));
return false;
}
}
return true;
}
/* 不合法 */
if (!isAccessTokenValid) {
const localTokenName = env.TokenLocalStorageName;
localStorage.removeItem(localTokenName);
api.removeAccessToken();
dispatch(postGlobalReduxReset());
return false;
}
return false;
};
+6
View File
@@ -0,0 +1,6 @@
export interface APIError {
message: string;
code: number;
errorStack: string;
errorMessage: string;
}
+9
View File
@@ -0,0 +1,9 @@
enum Dashboard {
SET_DASHBOARD_INITIALIZE = 'SET_DASHBOARD_INITIALIZE',
SET_DASHBOARD_COUNT = 'SET_DASHBOARD_COUNT',
SET_DASHBOARD_VARIATION_LIST = 'SET_DASHBOARD_VARIATION_LIST',
SET_DASHBOARD_UN_REPLY_LIST = 'SET_DASHBOARD_UN_REPLY_LIST',
SET_DASHBOARD_UN_FINISHED_LIST = 'SET_DASHBOARD_UN_FINISHED_LIST'
}
export default Dashboard;
+6
View File
@@ -0,0 +1,6 @@
enum Department {
SET_DEPARTMENT_INITIALIZE = 'SET_DEPARTMENT_INITIALIZE',
SET_DEPARTMENT_LIST = 'SET_DEPARTMENT_LIST'
}
export default Department;
+9
View File
@@ -0,0 +1,9 @@
enum Global {
SET_GLOBAL_INITIALIZE = 'SET_GLOBAL_INITIALIZE',
SET_GLOBAL_REDUX_RESET = 'SET_GLOBAL_REDUX_RESET',
SET_GLOBAL_LOADING = 'SET_GLOBAL_LOADING',
SET_GLOBAL_SIDE_BAR = 'SET_GLOBAL_SIDE_BAR',
SET_GLOBAL_SIDE_BAR_STATIC = 'SET_GLOBAL_SIDE_BAR_STATIC'
}
export default Global;
@@ -0,0 +1,24 @@
enum InternalRegulation {
SET_INTERNAL_REGULATION_INITIALIZE = 'SET_INTERNAL_REGULATION_INITIALIZE',
SET_INTERNAL_REGULATION_LIST = 'SET_INTERNAL_REGULATION_LIST',
SET_INTERNAL_REGULATION_DEPARTMENT_LIST = 'SET_INTERNAL_REGULATION_DEPARTMENT_LIST',
SET_INTERNAL_REGULATION_ERROR_LIST = 'SET_INTERNAL_REGULATION_ERROR_LIST',
CLEAR_INTERNAL_REGULATION_ERROR_LIST = 'CLEAR_INTERNAL_REGULATION_ERROR_LIST',
SET_INTERNAL_REGULATION_COMPARES_ERROR_LIST = 'SET_INTERNAL_REGULATION_COMPARES_ERROR_LIST',
CLEAR_INTERNAL_REGULATION_COMPARES_ERROR_LIST = 'CLEAR_INTERNAL_REGULATION_COMPARES_ERROR_LIST',
SET_INTERNAL_REGULATION_DETAIL = 'SET_INTERNAL_REGULATION_DETAIL',
SET_INTERNAL_REGULATION_NEW_VERSION_ERROR_LIST = 'SET_INTERNAL_REGULATION_NEW_VERSION_ERROR_LIST',
CLEAR_INTERNAL_REGULATION_NEW_VERSION_ERROR_LIST = 'CLEAR_INTERNAL_REGULATION_NEW_VERSION_ERROR_LIST',
SET_INTERNAL_REGULATION_RELATION_LIST = 'SET_INTERNAL_REGULATION_RELATION_LIST',
SET_INTERNAL_REGULATION_RELATION_SEARCH_LAW_LIST = 'SET_INTERNAL_REGULATION_RELATION_SEARCH_LAW_LIST',
CLEAN_INTERNAL_REGULATION_RELATION_SEARCH_LAW_LIST = 'CLEAN_INTERNAL_REGULATION_RELATION_SEARCH_LAW_LIST',
SET_INTERNAL_REGULATION_RELATION_SEARCH_LAW_DETAIL = 'SET_INTERNAL_REGULATION_RELATION_SEARCH_LAW_DETAIL',
SET_INTERNAL_REGULATION_RELATION_SEARCH_ARTICLE_LIST = 'SET_INTERNAL_REGULATION_RELATION_SEARCH_ARTICLE_LIST',
CLEAN_INTERNAL_REGULATION_RELATION_SEARCH_ARTICLE_LIST = 'CLEAN_INTERNAL_REGULATION_RELATION_SEARCH_ARTICLE_LIST',
SET_INTERNAL_REGULATION_RELATION_ARCHIVE_LIST = 'SET_INTERNAL_REGULATION_RELATION_ARCHIVE_LIST',
SET_INTERNAL_REGULATION_RELATION_ARCHIVE_URL_LIST = 'SET_INTERNAL_REGULATION_RELATION_ARCHIVE_URL_LIST',
SET_INTERNAL_REGULATION_VERSION_URL_LIST = 'SET_INTERNAL_REGULATION_VERSION_URL_LIST',
SET_INTERNAL_REGULATION_DOWNLOAD_URL_LIST = 'SET_INTERNAL_REGULATION_DOWNLOAD_URL_LIST'
}
export default InternalRegulation;
@@ -0,0 +1,22 @@
enum LawChangeNotification {
SET_LAW_CHANGE_NOTIFICATION_INITIALIZE = 'SET_LAW_CHANGE_NOTIFICATION_INITIALIZE',
SET_LAW_CHANGE_NOTIFICATION_UNPROCESSED = 'SET_LAW_CHANGE_NOTIFICATION_UNPROCESSED',
SET_LAW_CHANGE_NOTIFICATION_ALL_LIST = 'SET_LAW_CHANGE_NOTIFICATION_ALL_LIST',
SET_LAW_CHANGE_NOTIFICATION_LAW_LIST = 'SET_LAW_CHANGE_NOTIFICATION_LAW_LIST',
SET_LAW_CHANGE_NOTIFICATION_LAW_DETAIL = 'SET_LAW_CHANGE_NOTIFICATION_LAW_DETAIL',
SET_LAW_CHANGE_NOTIFICATION_LAW_RELATION_LIST = 'SET_LAW_CHANGE_NOTIFICATION_LAW_RELATION_LIST',
SET_LAW_CHANGE_NOTIFICATION_LAW_ISSUE = 'SET_LAW_CHANGE_NOTIFICATION_LAW_ISSUE',
SET_LAW_CHANGE_NOTIFICATION_ITP_LIST = 'SET_LAW_CHANGE_NOTIFICATION_ITP_LIST',
SET_LAW_CHANGE_NOTIFICATION_ITP_DETAIL = 'SET_LAW_CHANGE_NOTIFICATION_ITP_DETAIL',
SET_LAW_CHANGE_NOTIFICATION_DRAFT_LIST = 'SET_LAW_CHANGE_NOTIFICATION_DRAFT_LIST',
SET_LAW_CHANGE_NOTIFICATION_DRAFT_DETAIL = 'SET_LAW_CHANGE_NOTIFICATION_DRAFT_DETAIL',
SET_LAW_CHANGE_NOTIFICATION_ITP_PARSE_NUM = 'SET_LAW_CHANGE_NOTIFICATION_ITP_PARSE_NUM',
SET_LAW_CHANGE_NOTIFICATION_ITP_SEARCH_ISSUE_LIST = 'SET_LAW_CHANGE_NOTIFICATION_ITP_SEARCH_ISSUE_LIST',
CLEAR_LAW_CHANGE_NOTIFICATION_ITP_SEARCH_ISSUE_LIST = 'CLEAR_LAW_CHANGE_NOTIFICATION_ITP_SEARCH_ISSUE_LIST',
SET_LAW_CHANGE_NOTIFICATION_ITP_SEARCH_LAW_LIST = 'SET_LAW_CHANGE_NOTIFICATION_ITP_SEARCH_LAW_LIST',
CLEAR_LAW_CHANGE_NOTIFICATION_ITP_SEARCH_LAW_LIST = 'CLEAR_LAW_CHANGE_NOTIFICATION_ITP_SEARCH_LAW_LIST',
SET_LAW_CHANGE_NOTIFICATION_ITP_SEARCH_LAW_DETAIL = 'SET_LAW_CHANGE_NOTIFICATION_ITP_SEARCH_LAW_DETAIL',
SET_LAW_CHANGE_NOTIFICATION_ITP_CREATE_RECORD = 'SET_LAW_CHANGE_NOTIFICATION_ITP_CREATE_RECORD'
}
export default LawChangeNotification;
+9
View File
@@ -0,0 +1,9 @@
enum Message {
SET_MESSAGE_INITIALIZE = 'SET_MESSAGE_INITIALIZE',
SET_MESSAGE_DIALOG = 'SET_MESSAGE_DIALOG',
REMOVE_MESSAGE_DIALOG = 'REMOVE_MESSAGE_DIALOG',
SET_MESSAGE_CONFIRM = 'SET_MESSAGE_CONFIRM',
REMOVE_MESSAGE_CONFIRM = 'REMOVE_MESSAGE_CONFIRM',
}
export default Message;
+12
View File
@@ -0,0 +1,12 @@
enum Permission {
SET_PERMISSION_INITIALIZE = 'SET_PERMISSION_INITIALIZE',
SET_PERMISSION_DEPARTMENT_LIST = 'SET_PERMISSION_DEPARTMENT_LIST',
SET_PERMISSION_DEPARTMENT_DETAIL = 'SET_PERMISSION_DEPARTMENT_DETAIL',
SET_PERMISSION_ACCOUNT_LIST = 'SET_PERMISSION_ACCOUNT_LIST',
SET_PERMISSION_ACCOUNT_DETAIL = 'SET_PERMISSION_ACCOUNT_DETAIL',
SET_PERMISSION_ACCOUNT_SEARCH_LIST = 'SET_PERMISSION_ACCOUNT_SEARCH_LIST',
CLEAR_PERMISSION_ACCOUNT_SEARCH_LIST = 'CLEAR_PERMISSION_ACCOUNT_SEARCH_LIST',
SET_PERMISSION_ACCOUNT_ROLE_LIST = 'SET_PERMISSION_ACCOUNT_ROLE_LIST'
}
export default Permission;
+10
View File
@@ -0,0 +1,10 @@
enum Report {
SET_REPORT_INITIALIZE = 'SET_REPORT_INITIALIZE',
SET_REPORT_TASK_SUMMARY_LIST = 'SET_REPORT_TASK_SUMMARY_LIST',
SET_REPORT_TRACKING_LIST = 'SET_REPORT_TRACKING_LIST',
SET_REPORT_TASK_SUMMARY_EXPORT_LIST = 'SET_REPORT_TASK_SUMMARY_EXPORT_LIST',
SET_REPORT_TASK_TRACKING_EXPORT_LIST = 'SET_REPORT_TASK_TRACKING_EXPORT_LIST',
SET_REPORT_TASK_CRA_EXPORT_LIST = 'SET_REPORT_TASK_CRA_EXPORT_LIST'
}
export default Report;
+21
View File
@@ -0,0 +1,21 @@
enum Task {
SET_TASK_INITIALIZE = 'SET_TASK_INITIALIZE',
SET_TASK_DEPARTMENT_LIST = 'SET_TASK_DEPARTMENT_LIST',
SET_TASK_DEPARTMENT_SUGGESTION_LIST = 'SET_TASK_DEPARTMENT_SUGGESTION_LIST',
CLEAR_SUGGESTION_LIST = 'CLEAR_SUGGESTION_LIST',
SET_TASK_CREATE_RECORD = 'SET_TASK_CREATE_RECORD',
SET_TASK_LIST = 'SET_TASK_LIST',
SET_TASK_ME_LIST = 'SET_TASK_ME_LIST',
SET_TASK_BU_LIST = 'SET_TASK_BU_LIST',
SET_TASK_DETAIL = 'SET_TASK_DETAIL',
SET_TASK_SUB_DETAIL = 'SET_TASK_SUB_DETAIL',
SET_TASK_CREATE_CHECKPOINT_LIST = 'SET_TASK_CREATE_CHECKPOINT_LIST',
SET_TASK_SEND_CHECKPOINT_LIST = 'SET_TASK_SEND_CHECKPOINT_LIST',
SET_TASK_REJECT_CHECKPOINT_LIST = 'SET_TASK_REJECT_CHECKPOINT_LIST',
SET_TASK_SUB_SEND_CHECKPOINT_LIST = 'SET_TASK_SUB_SEND_CHECKPOINT_LIST',
SET_TASK_SUB_REJECT_CHECKPOINT_LIST = 'SET_TASK_SUB_REJECT_CHECKPOINT_LIST',
SET_TASK_NOTIFIED_DEPARTMENT_LIST = 'SET_TASK_NOTIFIED_DEPARTMENT_LIST',
SET_TASK_LAW_CHANGE_NOTIFICATION_LIST = 'SET_TASK_LAW_CHANGE_NOTIFICATION_LIST'
}
export default Task;
+10
View File
@@ -0,0 +1,10 @@
enum User {
SET_USER_INITIALIZE = 'SET_USER_INITIALIZE',
SET_USER_LOGIN_STATE = 'SET_USER_LOGIN_STATE',
SET_USER_AUTH_STATE = 'SET_USER_AUTH_STATE',
SET_USER_TOKEN = 'SET_USER_TOKEN',
SET_USER_ACCOUNT_INFO = 'SET_USER_ACCOUNT_INFO',
SET_USER_OAUTH_URL = 'SET_USER_OAUTH_URL',
}
export default User;
@@ -0,0 +1,15 @@
import Global from '@Models/Redux/Global';
import Message from '@Models/Redux/Message';
import User from '@Models/Redux/User';
import { MiddleWare } from '../_initializeMiddleware/types';
import { StoreState } from '../_InitializeStore/types';
function createInitial(middleWare: MiddleWare): StoreState {
return {
global: new Global(middleWare),
message: new Message(),
user: new User(),
};
}
export default createInitial;
@@ -0,0 +1,30 @@
import {
Store, createStore as reduxCreateStore, combineReducers, applyMiddleware,
} from 'redux';
import thunk from 'redux-thunk';
import { createGlobalReducer } from '@Reducers/global';
import { createMessageReducer } from '@Reducers/message';
import { createUserReducer } from '@Reducers/user';
import { CreateStoreParams } from './types';
function createAppStore(params: CreateStoreParams): Store {
const { initialState, initialMiddleWare } = params;
return reduxCreateStore(
combineReducers({
global: createGlobalReducer({
initialState: initialState.global,
}),
message: createMessageReducer({
initialState: initialState.message,
}),
user: createUserReducer({
initialState: initialState.user,
}),
}),
applyMiddleware(
thunk.withExtraArgument({ ...initialMiddleWare }),
),
);
}
export default createAppStore;
+18
View File
@@ -0,0 +1,18 @@
import { Store } from 'redux';
import { MiddleWare } from '@Reducers/_initializeMiddleware/types';
import { GlobalState } from '../global/types';
import { MessageState } from '../message/types';
import { UserState } from '../user/types';
export interface StoreState {
global: GlobalState;
message: MessageState;
user: UserState;
}
export interface CreateStoreParams {
initialState: StoreState;
initialMiddleWare: MiddleWare;
}
export type GetState = Store<StoreState>['getState'];
@@ -0,0 +1,14 @@
import API from '@API/index';
import Env from '@Env/index';
import { MiddleWare } from './types';
function createMiddleware(): MiddleWare {
const ENVInstance = new Env();
const APIInstance = new API({ host: ENVInstance.HostApiUrl });
return {
env: ENVInstance,
api: APIInstance,
};
}
export default createMiddleware;
+7
View File
@@ -0,0 +1,7 @@
import API from '@API/index';
import Env from '@Env/index';
export interface MiddleWare {
env: Env;
api: API;
}
+33
View File
@@ -0,0 +1,33 @@
import { Dispatch } from 'redux';
import { ThunkAction } from 'redux-thunk';
import { StoreState } from '@Reducers/_InitializeStore/types';
import GLOBAL_ACTION from '@Reducers/_Constants/Global';
import {
SetGlobalReduxResetAction,
SetGlobalLoadingAction,
} from './types';
/* Global */
export const SET_GLOBAL_LOADING = ({ loading }: { loading: boolean }): SetGlobalLoadingAction => ({
type: GLOBAL_ACTION.SET_GLOBAL_LOADING,
loading,
});
export const SET_GLOBAL_REDUX_RESET = (): SetGlobalReduxResetAction => ({
type: GLOBAL_ACTION.SET_GLOBAL_REDUX_RESET,
});
/* Global Action */
export const postGlobalReduxReset = (): ThunkAction<Promise<void>, StoreState, unknown, { type: string }> => async (
dispatch: Dispatch,
): Promise<void> => {
dispatch(SET_GLOBAL_REDUX_RESET());
};
export const postGlobalLoading = (
loading: boolean,
): ThunkAction<Promise<void>, StoreState, unknown, { type: string }> => async (dispatch: Dispatch): Promise<void> => {
dispatch(
SET_GLOBAL_LOADING({
loading,
}),
);
};
+23
View File
@@ -0,0 +1,23 @@
import { Reducer } from 'redux';
import produce from 'immer';
import GLOBAL_ACTION from '@Reducers/_Constants/Global';
import { CreateGlobalReducerParams, GlobalActionTypes, GlobalState } from './types';
export function createGlobalReducer(params: CreateGlobalReducerParams): Reducer<GlobalState, GlobalActionTypes> {
return (state = params.initialState, action: GlobalActionTypes): GlobalState => produce(state, (_draft) => {
const draft = _draft;
switch (action.type) {
case GLOBAL_ACTION.SET_GLOBAL_INITIALIZE:
draft.initialize();
break;
case GLOBAL_ACTION.SET_GLOBAL_LOADING:
draft.updateGlobalLoading(action.loading);
break;
case GLOBAL_ACTION.SET_GLOBAL_REDUX_RESET:
draft.initialize();
break;
default:
break;
}
});
}

Some files were not shown because too many files have changed in this diff Show More