@@ -1,4 +1,5 @@ | |||
module.exports = { | |||
remoteUrl: 'http://139.198.180.242:9003/', | |||
// remoteUrl: 'https://www.locking.cn/', | |||
gatewayPort: 7888, | |||
}; |
@@ -7,7 +7,11 @@ const { Subject } = require('./tool'); | |||
let mainWindow; | |||
const socketMsgSubject = new Subject(); | |||
const socketSubjects = { | |||
onMessage: new Subject(), | |||
onError: new Subject(), | |||
onClose: new Subject(), | |||
}; | |||
function createWindow() { | |||
//创建窗口 | |||
@@ -52,14 +56,26 @@ function createWindow() { | |||
mainWindow.webContents.send('socket:on-message', message); | |||
}; | |||
const onSocketError = (...args) => { | |||
mainWindow.webContents.send('socket:on-error', ...args); | |||
}; | |||
const onSocketClose = (...args) => { | |||
mainWindow.webContents.send('socket:on-close', ...args); | |||
}; | |||
mainWindow.webContents.on('did-finish-load', () => { | |||
// 加载初始localStorage数据 | |||
mainWindow.webContents.send('initialStorageData', storage.getAllItem()); | |||
socketMsgSubject.add(onMessageReceive); | |||
socketSubjects.onMessage.add(onMessageReceive); | |||
socketSubjects.onError.add(onSocketError); | |||
socketSubjects.onClose.add(onSocketClose); | |||
}); | |||
mainWindow.on('closed', () => { | |||
mainWindow = null; | |||
socketMsgSubject.remove(onMessageReceive); | |||
socketSubjects.onMessage.remove(onMessageReceive); | |||
socketSubjects.onError.remove(onSocketError); | |||
socketSubjects.onClose.remove(onSocketClose); | |||
}); | |||
} | |||
@@ -117,6 +133,15 @@ app.on('window-all-closed', () => { | |||
} | |||
}); | |||
initialWebsocketEvents(function onMessage(message) { | |||
socketMsgSubject.notify(message); | |||
}); | |||
initialWebsocketEvents( | |||
ipcMain, | |||
function onMessage(message) { | |||
socketSubjects.onMessage.notify(message); | |||
}, | |||
function onError(...args) { | |||
socketSubjects.onError.notify(...args); | |||
}, | |||
function onClose(...args) { | |||
socketSubjects.onClose.notify(...args); | |||
}, | |||
); |
@@ -1,26 +1,43 @@ | |||
const io = require('ws'); | |||
const config = require('./config'); | |||
function initialWebsocket(onMessage, onError) { | |||
const socket = new io( | |||
`ws://127.0.0.1:${config.gatewayPort}/websocket/subscriptionTaskSync`, | |||
); | |||
const skUrl = `ws://127.0.0.1:${config.gatewayPort}/websocket/subscriptionTaskSync`; | |||
let socket; | |||
function initialWebsocket(onMessage, onError, onClose) { | |||
socket = new io(skUrl); | |||
socket.on('open', () => { | |||
// socket.emit("hello", "world"); | |||
console.log('socket connection'); | |||
socket.on('message', onMessage); | |||
// socket.send('1'); | |||
}); | |||
socket.on('message', (message) => { | |||
console.log('socket message', message); | |||
onMessage(message); | |||
}); | |||
socket.on('error', (...args) => { | |||
console.log('socket error:', args); | |||
onError && onError(...args); | |||
}); | |||
socket.on('close', (...args) => { | |||
console.log('socket close:', args); | |||
onClose && onClose(...args); | |||
setTimeout(() => initialWebsocket(onMessage, onError, onClose), 5000); | |||
}); | |||
} | |||
module.exports.initialWebsocketEvents = function initialWebsocketEvents( | |||
ipcMain, | |||
onMessage, | |||
onError, | |||
onClose, | |||
) { | |||
initialWebsocket(onMessage, onError); | |||
// ipcMain.handle('socket:on', ) | |||
initialWebsocket(onMessage, onError, onClose); | |||
ipcMain.handle('socket:send-message', (event, message) => { | |||
if (socket && socket.readyState === io.OPEN) { | |||
console.log('socket send message to gateway:', message); | |||
socket.send(message); | |||
} else { | |||
console.log('socket status not ready:', socket.readyState); | |||
} | |||
}); | |||
}; |
@@ -5,6 +5,7 @@ import { firstCharToLowerCase, handleRequest } from './utils/tool'; | |||
import { isObject } from 'lodash'; | |||
import { logout, queryCurrent } from './services/user'; | |||
import storage from './utils/storage'; | |||
import './services/socket'; | |||
const codeMessage = { | |||
200: '服务器成功返回请求的数据。', | |||
@@ -7,38 +7,58 @@ import ATooltip from '../Tooltip'; | |||
import styles from './FileStatus.less'; | |||
import { CloseCircleFilled, CheckCircleFilled } from '@ant-design/icons'; | |||
import { Progress, Button } from 'antd'; | |||
import { TaskStatus, TaskType } from '@/services/API.helper'; | |||
import { DATA } from '@/services/API'; | |||
import { identity } from 'lodash'; | |||
interface FileStatusProps { | |||
className?: string; | |||
style?: CSSProperties; | |||
data: DATA.SocketFileMsg; | |||
} | |||
export default function FileStatus(props: FileStatusProps) { | |||
const { className, style, data, ...restProps } = props; | |||
console.log(data); | |||
export default function FileStatus(props: FileStatusProps & LoadDescProps) { | |||
const { className, style, ...restProps } = props; | |||
const filePath = useMemo(() => { | |||
return [data.projName, data.nodeName, data.relativePath] | |||
.filter(identity) | |||
.join(' / '); | |||
}, [data]); | |||
const relationModifier = useMemo(() => { | |||
return data.modifyName | |||
? `${data.modifyName}同步` | |||
: data.createUserName | |||
? `${data.createUserName}同步` | |||
: ''; | |||
}, [data]); | |||
return ( | |||
<div className={classNames(styles.fileStatus, className)} style={style}> | |||
<div className={styles.left}> | |||
<FileIcon className={styles.icon} extension="folder" /> | |||
<FileIcon className={styles.icon} extension={data.extension} /> | |||
<div className={styles.content}> | |||
<ATooltip placement="top" title="123"> | |||
<div className={styles.fileName}>文件名称文件名称文件名称文件名称文件名称文件名称文件名称文件名称文件名称文件名称文件名称文件名称</div> | |||
<ATooltip placement="top" title={data.archName}> | |||
<div className={styles.fileName}>{data.archName}</div> | |||
</ATooltip> | |||
<div className={styles.subContent}> | |||
<ATooltip placement="bottom" title="456"> | |||
<div className={styles.filePath}>文件路径文件路径文件路径文件路径文件路径文件路径</div> | |||
<ATooltip placement="bottom" title={filePath}> | |||
<div className={styles.filePath}>{filePath}</div> | |||
</ATooltip> | |||
<div className={styles.modifyInfo}>XX创建/XX同步</div> | |||
<div className={styles.modifyInfo}>{relationModifier}</div> | |||
</div> | |||
</div> | |||
</div> | |||
<div className={styles.mid}> | |||
<LoadDesc {...restProps} /> | |||
{ | |||
restProps.loadingState === 'complete' | |||
? <Time time="2020-01-01 18:24:56" /> | |||
: null | |||
} | |||
<LoadDesc | |||
type={data.taskType} | |||
loadingState={data.taskSyncStatus} | |||
progress={data.taskSyncProgress} | |||
{...restProps} | |||
/> | |||
{data.taskSyncStatus === TaskStatus.FINISH ? ( | |||
<Time time={'2020-01-01 18:24:56'} /> | |||
) : null} | |||
</div> | |||
<div className={styles.right}> | |||
{/* 查看1: 已下载 文件打开文件夹 */} | |||
@@ -47,11 +67,11 @@ export default function FileStatus(props: FileStatusProps & LoadDescProps) { | |||
{/* 重新下载: 下载失败时出现 */} | |||
{/* 重新上传: 上传失败时出现 */} | |||
{/* <Button type="link" className={styles.button} size="small">重新上传</Button> */} | |||
<Button type="link" className={styles.button} size="small">暂停</Button> | |||
<Button type="link" className={styles.button} size="small">取消</Button> | |||
{/* <Button type="link" className={styles.button} size="small">暂停</Button> */} | |||
{/* <Button type="link" className={styles.button} size="small">取消</Button> */} | |||
</div> | |||
</div> | |||
) | |||
); | |||
} | |||
const Time = memo((props: { time: string }) => ( | |||
@@ -59,33 +79,48 @@ const Time = memo((props: { time: string }) => ( | |||
)); | |||
interface LoadDescProps { | |||
type: 'upload' | 'download'; | |||
loadingState: 'loading' | 'complete'; | |||
type: TaskType; | |||
loadingState: TaskStatus; | |||
result?: 'success' | 'fail'; | |||
progress?: number; | |||
} | |||
function LoadDesc(props: LoadDescProps) { | |||
const { result, type, loadingState, progress } = props; | |||
const keywords = type === 'upload' ? '上传' : '下载'; | |||
const keywords = type === 'UPLOAD' ? '上传' : '下载'; | |||
const [resultText, icon] = useMemo(() => { | |||
if (result === 'success') { return ['成功', <CheckCircleFilled className={classNames(styles.stateIcon, styles.success)} />]; } | |||
if (result === 'fail') { return ['失败', <CloseCircleFilled className={classNames(styles.stateIcon, styles.error)} />]; } | |||
if (loadingState === TaskStatus.FINISH) { | |||
return [ | |||
'成功!', | |||
<CheckCircleFilled | |||
className={classNames(styles.stateIcon, styles.success)} | |||
/>, | |||
]; | |||
} | |||
if (loadingState === TaskStatus.FAILED) { | |||
return [ | |||
'失败!', | |||
<CloseCircleFilled | |||
className={classNames(styles.stateIcon, styles.error)} | |||
/>, | |||
]; | |||
} | |||
if (loadingState === TaskStatus.WAITING) { | |||
return [`等待${keywords}`]; | |||
} | |||
return ['', null]; | |||
}, [result]); | |||
}, [result, keywords]); | |||
return ( | |||
<span className={styles.loadDesc}> | |||
{ | |||
loadingState === 'loading' | |||
? <Progress percent={progress} format={() => `${keywords}中...`} /> | |||
: ( | |||
<> | |||
{icon} | |||
{keywords} | |||
{resultText}! | |||
</> | |||
) | |||
} | |||
{loadingState === TaskStatus.SYNCING ? ( | |||
<Progress percent={progress} format={() => `${keywords}中...`} /> | |||
) : ( | |||
<> | |||
{icon} | |||
{keywords} | |||
{resultText} | |||
</> | |||
)} | |||
</span> | |||
) | |||
} | |||
); | |||
} |
@@ -5,7 +5,9 @@ import { useState } from 'react'; | |||
import styles from './SyncModal.less'; | |||
import ATooltip from '../Tooltip'; | |||
import { fetchApi } from '@/utils/request'; | |||
import { firstCharToLowerCase } from '@/utils/tool'; | |||
import { firstCharToLowerCase, handleRequest } from '@/utils/tool'; | |||
import { useCallback } from 'react'; | |||
import system from '@/services/system'; | |||
const columns = [ | |||
{ | |||
@@ -43,13 +45,24 @@ interface DataType { | |||
export default function SyncModal() { | |||
const { initialState: { currentUser } = {} } = useModel('@@initialState'); | |||
const [btnLoading, setBtnLoading] = useState(false); | |||
const [modalVisible, setModalVisible] = useState(true); | |||
const [selectedKeys, setSelectedKeys] = useState<string[]>([]); | |||
const { loading, data } = useRequest(async () => { | |||
return await fetchApi('project/queryProjectListByUserId', { | |||
userId: currentUser!.id, | |||
}); | |||
}); | |||
const onOk = useCallback(async () => { | |||
setBtnLoading(true); | |||
const res = await system.syncProjects(selectedKeys); | |||
setBtnLoading(false); | |||
handleRequest(res!).success(() => { | |||
setModalVisible(false); | |||
}); | |||
}, [selectedKeys]); | |||
return ( | |||
<Modal | |||
title="同步文件夹至工作空间" | |||
@@ -59,6 +72,11 @@ export default function SyncModal() { | |||
okText="确定" | |||
width={540} | |||
onCancel={() => setModalVisible(false)} | |||
okButtonProps={{ | |||
disabled: selectedKeys.length === 0, | |||
loading: btnLoading, | |||
}} | |||
onOk={onOk} | |||
> | |||
<div className={styles.title}> | |||
选择「与我有关的文件夹」,并同步到「我的电脑/工作空间」中 | |||
@@ -78,11 +96,12 @@ export default function SyncModal() { | |||
'selectedRows: ', | |||
selectedRows, | |||
); | |||
setSelectedKeys(selectedRowKeys as string[]); | |||
}, | |||
getCheckboxProps: (record: DataType) => ({ | |||
disabled: record.name === 'Disabled User', // Column configuration not to be checked | |||
name: record.name, | |||
}), | |||
// getCheckboxProps: (record: DataType) => ({ | |||
// disabled: record.name === 'Disabled User', // Column configuration not to be checked | |||
// name: record.name, | |||
// }), | |||
}} | |||
pagination={false} | |||
columns={columns} | |||
@@ -6,6 +6,11 @@ import syncSuccessImg from './assets/sync.success.png'; | |||
import syncErrorImg from './assets/sync.error.png'; | |||
import classNames from 'classnames'; | |||
import SyncModal from '@/components/SyncModal'; | |||
import { useSocketMessages } from '@/services/socket'; | |||
import FileStatus from '@/components/FileStatus'; | |||
import { useMemo } from 'react'; | |||
import { TaskStatus } from '@/services/API.helper'; | |||
import { DATA } from '@/services/API'; | |||
enum SyncType { | |||
Syncing, | |||
@@ -20,7 +25,36 @@ const syncTypeList = [ | |||
]; | |||
export default function SyncView() { | |||
const originList = useSocketMessages(); | |||
const [type, setType] = useState<SyncType>(SyncType.Syncing); | |||
const [syncingList, succList, failList] = useMemo(() => { | |||
return originList.reduce( | |||
(arr, item) => { | |||
if (item.taskSyncStatus === TaskStatus.FINISH) { | |||
arr[1].push(item); | |||
return arr; | |||
} else if (item.taskSyncStatus === TaskStatus.FAILED) { | |||
arr[2].push(item); | |||
return arr; | |||
} | |||
arr[0].push(item); | |||
return arr; | |||
}, | |||
[[], [], []] as [ | |||
DATA.SocketFileMsg[], | |||
DATA.SocketFileMsg[], | |||
DATA.SocketFileMsg[], | |||
], | |||
); | |||
}, [originList]); | |||
const curretList = useMemo(() => { | |||
if (type === SyncType.SyncSuccess) return succList; | |||
if (type === SyncType.SyncError) return failList; | |||
return syncingList; | |||
}, [type, succList, failList, syncingList]); | |||
return ( | |||
<div className={styles.sync}> | |||
<div className={styles.left}> | |||
@@ -43,9 +77,14 @@ export default function SyncView() { | |||
))} | |||
</div> | |||
<div className={styles.right}> | |||
同步任务界面 | |||
<SyncModal /> | |||
<div className={styles.date}> | |||
<div>2021年6月14日</div> | |||
</div> | |||
{curretList.map((data) => ( | |||
<FileStatus key={data.taskId} className={styles.item} data={data} /> | |||
))} | |||
</div> | |||
<SyncModal /> | |||
</div> | |||
); | |||
} |
@@ -13,7 +13,10 @@ | |||
padding-left: 12px; | |||
line-height: 32px; | |||
margin-bottom: 0; | |||
span { display: block; transform: scale(0.83); } | |||
span { | |||
display: block; | |||
transform: scale(0.83); | |||
} | |||
} | |||
.tab { | |||
position: relative; | |||
@@ -30,13 +33,40 @@ | |||
border-right-color: @primary-color; | |||
background: rgba(120, 80, 255, 0.04); | |||
} | |||
.icon { flex: none; } | |||
.label { flex: 0 0 48px; margin: 0 8px; font-size: 12px; color: rgba(#000, 0.85); } | |||
.count { flex: 1; font-size: 12px; color: #8E909F; transform: scale(0.83); } | |||
.icon { | |||
flex: none; | |||
} | |||
.label { | |||
flex: 0 0 48px; | |||
margin: 0 8px; | |||
font-size: 12px; | |||
color: rgba(#000, 0.85); | |||
} | |||
.count { | |||
flex: 1; | |||
font-size: 12px; | |||
color: #8e909f; | |||
transform: scale(0.83); | |||
} | |||
} | |||
} | |||
.right { | |||
flex: 1; | |||
overflow: auto; | |||
padding: 0 12px; | |||
.item ~ .item { | |||
margin-top: 12px; | |||
margin-bottom: 8px; | |||
} | |||
.date { | |||
height: 36px; | |||
line-height: 36px; | |||
font-size: 12px; | |||
color: rgba(0, 0, 0, 0.8); | |||
> div { | |||
transform: scale(0.83); | |||
transform-origin: left; | |||
} | |||
} | |||
} | |||
} | |||
} |
@@ -1,3 +1,5 @@ | |||
import { TaskStatus, TaskType } from './API.helper'; | |||
declare namespace API { | |||
export interface ResponseData<T> { | |||
code: number | string; | |||
@@ -8,7 +10,8 @@ declare namespace API { | |||
*/ | |||
success: boolean; | |||
/** | |||
* 用于标明业务请求内容是否成功, 等价于code === 0 | |||
* 用于标明业务请求内容是否成功, | |||
* 等价于code === 0 | |||
*/ | |||
requestIsSuccess: boolean; | |||
} | |||
@@ -38,4 +41,40 @@ declare namespace DATA { | |||
*/ | |||
phone: string; | |||
} | |||
export interface SocketFileMsg { | |||
archName: string; | |||
commonStatus: number; // 1, | |||
createTime: string; // "2021-04-16T19:33:36+08:00", | |||
createUserId: string; // "367294106252087297", | |||
createUserName: string; // "", | |||
deleted: number; // 0, | |||
extension: string; // "pdf", | |||
fileSize: number; // 9385910, | |||
folderId: string; // "389513225458573314", | |||
folderLevelId: string; // "389513225454379009_342050628186824706", | |||
id: string; // "389513618179645440", | |||
ipfsCid: string; // "QmPYXHLuXoTF7A1BxsgTRVByojMXyWjjrrZS5MQeTX1mNq", | |||
isShowRecycle: number; // 0, | |||
milestone: number; // 1, | |||
modifyName: string; // "", | |||
modifyTime: string; // "2021-04-16T19:36:28+08:00", | |||
modifyUserId: string; // "0", | |||
nodeName: string; // "产品分析", | |||
projId: string; // "389513225454379008", | |||
projName: string; // "2021416-1", | |||
relativePath: string; // "", | |||
showUrl: string; // "", | |||
status: number; // 2, | |||
syncFileSize: number; // 9385910, | |||
syncIpfsCid: string; // "QmPYXHLuXoTF7A1BxsgTRVByojMXyWjjrrZS5MQeTX1mNq", | |||
syncVersion: number; // 1, | |||
taskId: string; // 420964845295652864, | |||
taskIsModify: boolean; // false, | |||
taskSyncProgress: 100; | |||
taskSyncStatus: TaskStatus; // "TASK_SYNC_STATUS_FINISH", | |||
taskType: TaskType; // "DOWNLOAD", | |||
version: number; // 1, | |||
workStatus: number; // 1 | |||
} | |||
} |
@@ -0,0 +1,28 @@ | |||
export enum TaskStatus { | |||
/** | |||
* 等待同步 | |||
*/ | |||
WAITING = 'TASK_SYNC_STATUS_WAIT', | |||
/** | |||
* 同步中 | |||
*/ | |||
SYNCING = 'TASK_SYNC_STATUS_ING', | |||
/** | |||
* 同步暂停 | |||
*/ | |||
PAUSED = 'TASK_SYNC_STATUS_PAUSE', | |||
/** | |||
* 同步成功 | |||
*/ | |||
FINISH = 'TASK_SYNC_STATUS_FINISH', | |||
/** | |||
* 同步失败 | |||
*/ | |||
FAILED = 'TASK_SYNC_STATUS_FAIL', | |||
} | |||
export enum TaskType { | |||
// 下载 | |||
DOWNLOAD = 'DOWNLOAD', | |||
// 上传 | |||
UPLOAD = 'UPLOAD', | |||
} |
@@ -1,11 +1,65 @@ | |||
import { firstCharToLowerCase, Subject } from '@/utils/tool'; | |||
import { useEffect, useState } from 'react'; | |||
import { DATA } from './API'; | |||
export const isClient = !!window.ipcRenderer; // process.env.IS_CLIENT; | |||
const safeCall = (f: (...args: any[]) => any) => (isClient ? f : () => {}); | |||
class FileMessageManager extends Subject { | |||
public list: DATA.SocketFileMsg[] = []; | |||
// 主键 taskId | |||
public fileMap = new Map(); | |||
addFileMsg(message: DATA.SocketFileMsg) { | |||
this.list = [message].concat(this.list); | |||
this.notifyObservers(this.list); | |||
} | |||
updateFileMsg(message: DATA.SocketFileMsg) { | |||
this.fileMap.set(message.taskId, message); | |||
this.list = this.list.map((msg) => | |||
msg.taskId === message.taskId ? message : msg, | |||
); | |||
this.notifyObservers(this.list); | |||
} | |||
receiveMessage(messageStr: string) { | |||
try { | |||
const message = firstCharToLowerCase( | |||
JSON.parse(messageStr), | |||
) as DATA.SocketFileMsg; | |||
const existMsg = this.fileMap.get(message.taskId); | |||
if (existMsg) { | |||
this.updateFileMsg(message); | |||
} else { | |||
this.addFileMsg(message); | |||
} | |||
} catch (e) { | |||
console.error('message 解析失败:', e, messageStr); | |||
} | |||
} | |||
} | |||
const ipcRenderer = window.ipcRenderer; | |||
const fileMng = new FileMessageManager(); | |||
if (isClient) { | |||
window.addIpcRendererListener('socket:on-message', (_, message) => { | |||
console.log('receive electron socket message:', message); | |||
fileMng.receiveMessage(message); | |||
}); | |||
window.addIpcRendererListener('socket:on-error', (_, ...args) => { | |||
console.log('receive electron socket error:', args); | |||
}); | |||
window.addIpcRendererListener('socket:on-close', (_, ...args) => { | |||
console.log('receive electron socket close:', args); | |||
}); | |||
} | |||
export function useSocketMessages() { | |||
const [list, setList] = useState(fileMng.list); | |||
useEffect(() => { | |||
fileMng.addObserver(setList); | |||
return () => { | |||
fileMng.removeObserver(setList); | |||
}; | |||
}, []); | |||
return list; | |||
} |
@@ -33,10 +33,26 @@ const system = { | |||
ipcRenderer.invoke('storage:remove', { key }); | |||
}), | |||
}, | |||
// 发往网关的请求 | |||
login: safeCall((userId: string, userPhone: string) => { | |||
return fetchLocalApi<null>('login', { userId, userPhone }); | |||
/* 发往网关的请求 */ | |||
// 登陆 | |||
login: safeCall(async (userId: string, userPhone: string) => { | |||
const res = await fetchLocalApi<null>( | |||
'login', | |||
{ userId, userPhone }, | |||
{ silent: true }, | |||
); | |||
ipcRenderer.invoke('socket:send-message', `login:${userPhone}`); | |||
return res; | |||
}), | |||
// 退出登陆 | |||
logout: safeCall(async () => { | |||
return await fetchLocalApi<null>('logout'); | |||
}), | |||
// 发送下载同步文件夹的指令 | |||
syncProjects: safeCall((projectIds: string[]) => { | |||
return fetchLocalApi<null>('syncFolderToWorkSpace', { | |||
ids: projectIds.join(','), | |||
}); | |||
}), | |||
}; | |||
@@ -4,6 +4,7 @@ import storage from '@/utils/storage'; | |||
import { errorReponse, firstCharToLowerCase, isReqSuccess } from '@/utils/tool'; | |||
import { propertyOf } from 'lodash'; | |||
import system, { isClient } from './system'; | |||
import { DATA } from './API'; | |||
export async function queryCurrent() { | |||
const accountId = storage.get('accountId'); | |||
@@ -69,6 +70,7 @@ export async function login(account: string, password: string) { | |||
export function logout() { | |||
storage.clear(); | |||
system.logout(); | |||
// window.location.href = '/login'; | |||
history.replace('/login'); | |||
// window.location.reload(); | |||
@@ -26,10 +26,15 @@ export async function fetchApi<T = any>( | |||
return res; | |||
} | |||
interface IOption { | |||
silent?: boolean; | |||
method?: 'GET' | 'POST'; | |||
} | |||
export async function fetchLocalApi<T = any>( | |||
path: string, | |||
params = {}, | |||
options = { silent: false, method: 'GET' }, | |||
options: IOption = { silent: false, method: 'GET' }, | |||
) { | |||
if (!gatewayPort) { | |||
return errorReponse('gateway infomation failed'); | |||
@@ -77,3 +77,20 @@ export const isValidPhone = (phone: string) => | |||
export const passwordReg = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[\S]{8,16}$/; | |||
export const isValidPassword = (maybePassword: string) => | |||
new RegExp(passwordReg).test(maybePassword); | |||
type Observer = (...args: any[]) => any; | |||
export class Subject { | |||
public observers: Array<Observer> = []; | |||
// constructor() { | |||
// this.observers = []; | |||
// } | |||
addObserver(observer: Observer) { | |||
this.observers.push(observer); | |||
} | |||
removeObserver(observer: Observer) { | |||
this.observers = this.observers.filter((iO) => iO !== observer); | |||
} | |||
notifyObservers(...args: unknown[]) { | |||
this.observers.forEach((f) => f(...args)); | |||
} | |||
} |