[feat] Initial code
This commit is contained in:
@@ -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;
|
||||
@@ -0,0 +1,5 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
export const createAxios = ({ host }: { host: string }): AxiosInstance => axios.create({
|
||||
baseURL: host,
|
||||
});
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
export interface HeaderContent {
|
||||
Authorization?: string;
|
||||
}
|
||||
@@ -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;
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
export interface APIParams {
|
||||
host: string;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Vendored
+27
@@ -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>;
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -0,0 +1 @@
|
||||
export const ENTER_KEY_CODE = 13;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,7 @@
|
||||
import Guest from './Guest';
|
||||
import Admin from './Admin';
|
||||
|
||||
export default {
|
||||
Guest,
|
||||
Admin,
|
||||
};
|
||||
Vendored
+23
@@ -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[];
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const TIMEZONE_TAIPEI = 'Asia/Taipei';
|
||||
@@ -0,0 +1,3 @@
|
||||
/* 首頁 */
|
||||
export const ROOT_PAGE_HOME = '/home';
|
||||
export const PAGE_HOME_OVERVIEW = '/overview';
|
||||
@@ -0,0 +1,5 @@
|
||||
/* 使用者登入 */
|
||||
export const BASE_PAGE_SIGN_IN = '/login';
|
||||
|
||||
/* 第三方登入 */
|
||||
export const BASE_PAGE_OAUTH = '/oauth_check';
|
||||
@@ -0,0 +1,7 @@
|
||||
import * as GUEST from './Guest';
|
||||
import * as ADMIN from './Admin';
|
||||
|
||||
export default {
|
||||
GUEST,
|
||||
ADMIN,
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
/* Component Style */
|
||||
:local(.appContainer) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
@@ -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);
|
||||
Vendored
+8
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
Vendored
+13
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
Vendored
+29
@@ -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);
|
||||
@@ -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);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { MessageDialogObject } from '@Models/Redux/Message/types';
|
||||
|
||||
export interface DialogItemObject {
|
||||
dialog: MessageDialogObject;
|
||||
requestRemoveDialog: () => void;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
Vendored
+12
@@ -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;
|
||||
Vendored
+12
@@ -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;
|
||||
Vendored
+57
@@ -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;
|
||||
Vendored
+8
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { useEffect, EffectCallback } from 'react';
|
||||
|
||||
export default function useDidMount(effect: EffectCallback): void {
|
||||
return useEffect(effect, []);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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}</>;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
}
|
||||
@@ -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],
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
@@ -0,0 +1,3 @@
|
||||
const lang = {};
|
||||
|
||||
export default lang;
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
export default ['zh-TW', 'en'];
|
||||
@@ -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;
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
export interface LangObject {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface LangListItem {
|
||||
lang: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type LangList = LangListItem[];
|
||||
@@ -0,0 +1,5 @@
|
||||
import { immerable } from 'immer';
|
||||
|
||||
export default class Immerable {
|
||||
protected [immerable] = true;
|
||||
}
|
||||
@@ -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 = '建立通知' | '不通知' | '重複' | '不適用' | '未查看' | '已查看';
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Vendored
+28
@@ -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[];
|
||||
}
|
||||
@@ -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;
|
||||
Vendored
+21
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Vendored
+69
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
export interface APIError {
|
||||
message: string;
|
||||
code: number;
|
||||
errorStack: string;
|
||||
errorMessage: string;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,6 @@
|
||||
enum Department {
|
||||
SET_DEPARTMENT_INITIALIZE = 'SET_DEPARTMENT_INITIALIZE',
|
||||
SET_DEPARTMENT_LIST = 'SET_DEPARTMENT_LIST'
|
||||
}
|
||||
|
||||
export default Department;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
@@ -0,0 +1,7 @@
|
||||
import API from '@API/index';
|
||||
import Env from '@Env/index';
|
||||
|
||||
export interface MiddleWare {
|
||||
env: Env;
|
||||
api: API;
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -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
Reference in New Issue
Block a user